MoleditPy 2.5.1__tar.gz → 2.6.0__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 (65) hide show
  1. {moleditpy-2.5.1 → moleditpy-2.6.0}/PKG-INFO +1 -1
  2. {moleditpy-2.5.1 → moleditpy-2.6.0}/pyproject.toml +2 -2
  3. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/SOURCES.txt +6 -5
  5. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/__main__.py +0 -3
  6. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/main.py +0 -3
  7. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/__init__.py +3 -3
  8. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/about_dialog.py +2 -2
  9. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/align_plane_dialog.py +6 -33
  10. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/alignment_dialog.py +3 -31
  11. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/angle_dialog.py +10 -87
  12. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/atom_item.py +2 -2
  13. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/bond_item.py +2 -6
  14. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/bond_length_dialog.py +10 -87
  15. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/calculation_worker.py +5 -36
  16. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/color_settings_dialog.py +6 -9
  17. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/constants.py +1 -1
  18. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/constrained_optimization_dialog.py +3 -14
  19. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/custom_interactor_style.py +1 -36
  20. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/custom_qt_interactor.py +0 -6
  21. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/dialog3_d_picking_mixin.py +69 -5
  22. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/dihedral_dialog.py +18 -102
  23. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window.py +2 -67
  24. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_app_state.py +4 -50
  25. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_compute.py +24 -86
  26. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_dialog_manager.py +3 -63
  27. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_edit_3d.py +7 -98
  28. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_edit_actions.py +6 -97
  29. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_export.py +22 -65
  30. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_main_init.py +35 -113
  31. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_molecular_parsers.py +34 -96
  32. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_project_io.py +34 -87
  33. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_string_importers.py +0 -35
  34. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_ui_manager.py +0 -70
  35. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_view_3d.py +0 -87
  36. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_view_loaders.py +0 -33
  37. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/mirror_dialog.py +2 -2
  38. moleditpy-2.6.0/src/moleditpy/modules/mol_geometry.py +165 -0
  39. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/molecule_scene.py +22 -48
  40. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/move_group_dialog.py +10 -12
  41. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/periodic_table_dialog.py +1 -1
  42. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/planarize_dialog.py +7 -25
  43. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/plugin_interface.py +0 -16
  44. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/plugin_manager_window.py +5 -13
  45. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/settings_dialog.py +8 -159
  46. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/template_preview_item.py +0 -1
  47. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/translation_dialog.py +4 -13
  48. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/user_template_dialog.py +2 -2
  49. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/zoomable_view.py +0 -1
  50. {moleditpy-2.5.1 → moleditpy-2.6.0}/LICENSE +0 -0
  51. {moleditpy-2.5.1 → moleditpy-2.6.0}/README.md +0 -0
  52. {moleditpy-2.5.1 → moleditpy-2.6.0}/setup.cfg +0 -0
  53. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  54. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  55. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/requires.txt +0 -0
  56. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/top_level.txt +0 -0
  57. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/__init__.py +0 -0
  58. {moleditpy-2.5.1/src/moleditpy/modules → moleditpy-2.6.0/src/moleditpy}/assets/file_icon.ico +0 -0
  59. {moleditpy-2.5.1/src/moleditpy/modules → moleditpy-2.6.0/src/moleditpy}/assets/icon.icns +0 -0
  60. {moleditpy-2.5.1/src/moleditpy/modules → moleditpy-2.6.0/src/moleditpy}/assets/icon.ico +0 -0
  61. {moleditpy-2.5.1/src/moleditpy/modules → moleditpy-2.6.0/src/moleditpy}/assets/icon.png +0 -0
  62. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/analysis_window.py +0 -0
  63. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/molecular_data.py +0 -0
  64. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/plugin_manager.py +0 -0
  65. {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/template_preview_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 2.5.1
3
+ Version: 2.6.0
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.1"
8
+ version = "2.6.0"
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.1
3
+ Version: 2.6.0
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
@@ -61,8 +66,4 @@ src/moleditpy/modules/template_preview_item.py
61
66
  src/moleditpy/modules/template_preview_view.py
62
67
  src/moleditpy/modules/translation_dialog.py
63
68
  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
69
+ src/moleditpy/modules/zoomable_view.py
@@ -16,11 +16,8 @@ print("MoleditPy — A Python-based molecular editing software")
16
16
  print("-----------------------------------------------------\n")
17
17
 
18
18
  try:
19
- # Preferred when running as a package: python -m moleditpy
20
19
  from .main import main
21
20
  except Exception:
22
- # Fallback when running the file directly: python __main__.py
23
- # This will import the top-level `main` module in the same folder.
24
21
  from main import main
25
22
 
26
23
  # --- Application Execution ---
@@ -17,11 +17,8 @@ import ctypes
17
17
  from PyQt6.QtWidgets import QApplication
18
18
 
19
19
  try:
20
- # When executed as part of the package (python -m moleditpy)
21
20
  from .modules.main_window import MainWindow
22
21
  except Exception:
23
- # When executed as a standalone script (python main.py) the package-relative
24
- # import won't work; fall back to absolute import that works with sys.path
25
22
  from modules.main_window import MainWindow
26
23
 
27
24
  def main():
@@ -15,7 +15,7 @@ DOI: 10.5281/zenodo.17268532
15
15
  # Do not import `pybel` at module import time. Only expose the presence
16
16
  # of Open Babel via `OBABEL_AVAILABLE`. Modules should import `pybel`
17
17
  # lazily if and when they need it.
18
- try:
18
+ try: # pragma: no cover
19
19
  import importlib.util
20
20
  OBABEL_AVAILABLE = importlib.util.find_spec("openbabel") is not None
21
21
  except Exception:
@@ -26,7 +26,7 @@ except Exception:
26
26
  # it once at module import time and expose a small, robust wrapper so callers
27
27
  # can avoid re-importing sip repeatedly and so we centralize exception
28
28
  # handling (this reduces crash risk during teardown and deletion operations).
29
- try:
29
+ try: # pragma: no cover
30
30
  import sip as _sip # type: ignore
31
31
  _sip_isdeleted = getattr(_sip, 'isdeleted', None)
32
32
  except Exception:
@@ -40,7 +40,7 @@ def sip_isdeleted_safe(obj) -> bool:
40
40
  occurs while checking, it returns False (i.e. not deleted) so that the
41
41
  caller can continue other lightweight guards (like checking scene()).
42
42
  """
43
- try:
43
+ try: # pragma: no cover
44
44
  if _sip_isdeleted is None:
45
45
  return False
46
46
  return bool(_sip_isdeleted(obj))
@@ -21,7 +21,7 @@ try:
21
21
  except Exception:
22
22
  from modules.constants import VERSION
23
23
 
24
- class AboutDialog(QDialog):
24
+ class AboutDialog(QDialog): # pragma: no cover
25
25
  def __init__(self, main_window, parent=None):
26
26
  super().__init__(parent)
27
27
  self.main_window = main_window
@@ -37,7 +37,7 @@ class AboutDialog(QDialog):
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)
@@ -21,7 +21,7 @@ try:
21
21
  except Exception:
22
22
  from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
23
23
 
24
- class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
24
+ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
25
25
  def __init__(self, mol, main_window, plane, preselected_atoms=None, parent=None):
26
26
  QDialog.__init__(self, parent)
27
27
  Dialog3DPickingMixin.__init__(self)
@@ -44,7 +44,7 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
44
44
  def init_ui(self):
45
45
  plane_names = {'xy': 'XY', 'xz': 'XZ', 'yz': 'YZ'}
46
46
  self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
47
- self.setModal(False) # モードレスにしてクリックを阻害しない
47
+ self.setModal(False)
48
48
  layout = QVBoxLayout(self)
49
49
 
50
50
  # Instructions
@@ -163,39 +163,12 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
163
163
 
164
164
  def show_atom_labels(self):
165
165
  """選択された原子にラベルを表示"""
166
- # 既存のラベルをクリア
167
- self.clear_atom_labels()
168
-
169
- # 新しいラベルを表示
170
- if not hasattr(self, 'selection_labels'):
171
- self.selection_labels = []
172
-
173
166
  if self.selected_atoms:
174
167
  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 = []
168
+ pairs = [(idx, f"#{i+1}") for i, idx in enumerate(sorted_atoms)]
169
+ self.show_atom_labels_for(pairs, color='blue')
170
+ else:
171
+ self.clear_atom_labels()
199
172
 
200
173
  def apply_PlaneAlign(self):
201
174
  """alignを適用(回転ベース)"""
@@ -13,7 +13,7 @@ DOI: 10.5281/zenodo.17268532
13
13
  from PyQt6.QtWidgets import (
14
14
  QDialog, QVBoxLayout, QLabel, QHBoxLayout, QPushButton
15
15
  )
16
- from PyQt6.QtCore import Qt
16
+
17
17
  from PyQt6.QtWidgets import QMessageBox
18
18
  import numpy as np
19
19
 
@@ -22,7 +22,7 @@ try:
22
22
  except Exception:
23
23
  from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
24
24
 
25
- class AlignmentDialog(Dialog3DPickingMixin, QDialog):
25
+ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
26
26
  def __init__(self, mol, main_window, axis, preselected_atoms=None, parent=None):
27
27
  QDialog.__init__(self, parent)
28
28
  Dialog3DPickingMixin.__init__(self)
@@ -46,7 +46,7 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
46
46
  def init_ui(self):
47
47
  axis_names = {'x': 'X-axis', 'y': 'Y-axis', 'z': 'Z-axis'}
48
48
  self.setWindowTitle(f"Align to {axis_names[self.axis]}")
49
- self.setModal(False) # モードレスにしてクリックを阻害しない
49
+ self.setModal(False)
50
50
  layout = QVBoxLayout(self)
51
51
 
52
52
  # Instructions
@@ -133,24 +133,6 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
133
133
  self.selected_atoms.clear()
134
134
  self.update_display()
135
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
-
154
136
  def remove_atom_label(self, atom_idx):
155
137
  """特定の原子のラベルを削除"""
156
138
  # 簡単化のため、全ラベルをクリアして再描画
@@ -159,16 +141,6 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
159
141
  if idx != atom_idx:
160
142
  self.add_selection_label(idx, f"Atom {i}")
161
143
 
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
-
172
144
  def apply_alignment(self):
173
145
  """アライメントを適用"""
174
146
  if len(self.selected_atoms) != 2:
@@ -16,15 +16,17 @@ from PyQt6.QtWidgets import (
16
16
 
17
17
  try:
18
18
  from .dialog3_d_picking_mixin import Dialog3DPickingMixin
19
+ from .mol_geometry import get_connected_group
19
20
  except Exception:
20
21
  from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
22
+ from modules.mol_geometry import get_connected_group
21
23
 
22
24
  from PyQt6.QtCore import Qt
23
25
  from PyQt6.QtWidgets import QMessageBox
24
26
  import numpy as np
25
27
 
26
28
 
27
- class AngleDialog(Dialog3DPickingMixin, QDialog):
29
+ class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
28
30
  def __init__(self, mol, main_window, preselected_atoms=None, parent=None):
29
31
  QDialog.__init__(self, parent)
30
32
  Dialog3DPickingMixin.__init__(self)
@@ -44,8 +46,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
44
46
 
45
47
  def init_ui(self):
46
48
  self.setWindowTitle("Adjust Angle")
47
- self.setModal(False) # モードレスにしてクリックを阻害しない
48
- # 常に前面表示
49
+ self.setModal(False)
49
50
  layout = QVBoxLayout(self)
50
51
 
51
52
  # Instructions
@@ -170,69 +171,10 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
170
171
 
171
172
  def show_atom_labels(self):
172
173
  """選択された原子にラベルを表示"""
173
- # 既存のラベルをクリア
174
- self.clear_atom_labels()
175
-
176
- # 新しいラベルを表示
177
- if not hasattr(self, 'selection_labels'):
178
- self.selection_labels = []
179
-
180
174
  selected_atoms = [self.atom1_idx, self.atom2_idx, self.atom3_idx]
181
175
  labels = ["1st", "2nd (vertex)", "3rd"]
182
- colors = ["yellow", "yellow", "yellow"] # 全て黄色に統一
183
-
184
- for i, atom_idx in enumerate(selected_atoms):
185
- if atom_idx is not None:
186
- pos = self.main_window.atom_positions_3d[atom_idx]
187
- label_text = f"{labels[i]}"
188
-
189
- # ラベルを追加
190
- label_actor = self.main_window.plotter.add_point_labels(
191
- [pos], [label_text],
192
- point_size=20,
193
- font_size=12,
194
- text_color=colors[i],
195
- always_visible=True
196
- )
197
- self.selection_labels.append(label_actor)
198
-
199
- def clear_atom_labels(self):
200
- """原子ラベルをクリア"""
201
- if hasattr(self, 'selection_labels'):
202
- for label_actor in self.selection_labels:
203
- try:
204
- self.main_window.plotter.remove_actor(label_actor)
205
- except Exception:
206
- pass
207
- self.selection_labels = []
208
-
209
- def clear_selection_labels(self):
210
- """選択ラベルをクリア"""
211
- if hasattr(self, 'selection_labels'):
212
- for label_actor in self.selection_labels:
213
- try:
214
- self.main_window.plotter.remove_actor(label_actor)
215
- except Exception:
216
- pass
217
- self.selection_labels = []
218
-
219
- def add_selection_label(self, atom_idx, label_text):
220
- """選択された原子にラベルを追加"""
221
- if not hasattr(self, 'selection_labels'):
222
- self.selection_labels = []
223
-
224
- # 原子の位置を取得
225
- pos = self.main_window.atom_positions_3d[atom_idx]
226
-
227
- # ラベルを追加
228
- label_actor = self.main_window.plotter.add_point_labels(
229
- [pos], [label_text],
230
- point_size=20,
231
- font_size=12,
232
- text_color='yellow',
233
- always_visible=True
234
- )
235
- self.selection_labels.append(label_actor)
176
+ pairs = [(idx, labels[i]) for i, idx in enumerate(selected_atoms) if idx is not None]
177
+ self.show_atom_labels_for(pairs)
236
178
 
237
179
  def update_display(self):
238
180
  """表示を更新"""
@@ -373,8 +315,8 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
373
315
  half_rotation = total_rotation_angle / 2
374
316
 
375
317
  # Get both connected groups
376
- group1_atoms = self.get_connected_group(self.atom1_idx, exclude=self.atom2_idx)
377
- group3_atoms = self.get_connected_group(self.atom3_idx, exclude=self.atom2_idx)
318
+ group1_atoms = get_connected_group(self.mol, self.atom1_idx, exclude=self.atom2_idx)
319
+ group3_atoms = get_connected_group(self.mol, self.atom3_idx, exclude=self.atom2_idx)
378
320
 
379
321
  # Rotate group 1 by -half_rotation
380
322
  for atom_idx in group1_atoms:
@@ -402,7 +344,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
402
344
  self.main_window.atom_positions_3d[self.atom3_idx] = new_pos3
403
345
  else:
404
346
  # Rotate the connected group around atom2 (vertex) - default behavior
405
- atoms_to_move = self.get_connected_group(self.atom3_idx, exclude=self.atom2_idx)
347
+ atoms_to_move = get_connected_group(self.mol, self.atom3_idx, exclude=self.atom2_idx)
406
348
 
407
349
  for atom_idx in atoms_to_move:
408
350
  current_pos = np.array(conf.GetAtomPosition(atom_idx))
@@ -418,23 +360,4 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
418
360
  # Update the 3D view
419
361
  self.main_window.draw_molecule_3d(self.mol)
420
362
 
421
- def get_connected_group(self, start_atom, exclude=None):
422
- """指定された原子から連結されているグループを取得"""
423
- visited = set()
424
- to_visit = [start_atom]
425
-
426
- while to_visit:
427
- current = to_visit.pop()
428
- if current in visited or current == exclude:
429
- continue
430
-
431
- visited.add(current)
432
-
433
- # Get neighboring atoms
434
- atom = self.mol.GetAtomWithIdx(current)
435
- for bond in atom.GetBonds():
436
- other_idx = bond.GetOtherAtomIdx(current)
437
- if other_idx not in visited and other_idx != exclude:
438
- to_visit.append(other_idx)
439
-
440
- return visited
363
+
@@ -23,13 +23,13 @@ from PyQt6.QtCore import (
23
23
  try:
24
24
  from .constants import (
25
25
  ATOM_RADIUS, DESIRED_ATOM_PIXEL_RADIUS,
26
- FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
26
+ FONT_FAMILY, FONT_WEIGHT_BOLD,
27
27
  CPK_COLORS,
28
28
  )
29
29
  except Exception:
30
30
  from modules.constants import (
31
31
  ATOM_RADIUS, DESIRED_ATOM_PIXEL_RADIUS,
32
- FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
32
+ FONT_FAMILY, FONT_WEIGHT_BOLD,
33
33
  CPK_COLORS,
34
34
  )
35
35
 
@@ -24,13 +24,13 @@ from PyQt6.QtCore import (
24
24
  try:
25
25
  from .constants import (
26
26
  EZ_LABEL_BOX_SIZE, EZ_LABEL_TEXT_OUTLINE, EZ_LABEL_MARGIN,
27
- BOND_OFFSET, FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
27
+ FONT_FAMILY, FONT_WEIGHT_BOLD,
28
28
  HOVER_PEN_WIDTH, DESIRED_BOND_PIXEL_WIDTH,
29
29
  )
30
30
  except Exception:
31
31
  from modules.constants import (
32
32
  EZ_LABEL_BOX_SIZE, EZ_LABEL_TEXT_OUTLINE, EZ_LABEL_MARGIN,
33
- BOND_OFFSET, FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
33
+ FONT_FAMILY, FONT_WEIGHT_BOLD,
34
34
  HOVER_PEN_WIDTH, DESIRED_BOND_PIXEL_WIDTH,
35
35
  )
36
36
 
@@ -206,9 +206,6 @@ class BondItem(QGraphicsItem):
206
206
  except Exception:
207
207
  return None
208
208
 
209
-
210
-
211
-
212
209
  def paint(self, painter, option, widget):
213
210
  if self.atom1 is None or self.atom2 is None:
214
211
  return
@@ -341,7 +338,6 @@ class BondItem(QGraphicsItem):
341
338
  if bond and bond.IsInRing():
342
339
  is_in_ring = True
343
340
  # 環の中心を計算(この結合を含む最小環)
344
- from rdkit import Chem
345
341
  ring_info = mol.GetRingInfo()
346
342
  for ring in ring_info.AtomRings():
347
343
  if rdkit_idx1 in ring and rdkit_idx2 in ring:
@@ -15,11 +15,12 @@ from PyQt6.QtWidgets import (
15
15
  )
16
16
 
17
17
  from .dialog3_d_picking_mixin import Dialog3DPickingMixin
18
+ from .mol_geometry import get_connected_group
18
19
 
19
20
  from PyQt6.QtCore import Qt
20
21
  import numpy as np
21
22
 
22
- class BondLengthDialog(Dialog3DPickingMixin, QDialog):
23
+ class BondLengthDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
23
24
  def __init__(self, mol, main_window, preselected_atoms=None, parent=None):
24
25
  QDialog.__init__(self, parent)
25
26
  Dialog3DPickingMixin.__init__(self)
@@ -37,8 +38,8 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
37
38
 
38
39
  def init_ui(self):
39
40
  self.setWindowTitle("Adjust Bond Length")
40
- self.setModal(False) # モードレスにしてクリックを阻害しない
41
- # 常に前面表示
41
+ self.setModal(False)
42
+
42
43
  layout = QVBoxLayout(self)
43
44
 
44
45
  # Instructions
@@ -158,69 +159,10 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
158
159
 
159
160
  def show_atom_labels(self):
160
161
  """選択された原子にラベルを表示"""
161
- # 既存のラベルをクリア
162
- self.clear_atom_labels()
163
-
164
- # 新しいラベルを表示
165
- if not hasattr(self, 'selection_labels'):
166
- self.selection_labels = []
167
-
168
162
  selected_atoms = [self.atom1_idx, self.atom2_idx]
169
163
  labels = ["1st", "2nd"]
170
- colors = ["yellow", "yellow"]
171
-
172
- for i, atom_idx in enumerate(selected_atoms):
173
- if atom_idx is not None:
174
- pos = self.main_window.atom_positions_3d[atom_idx]
175
- label_text = f"{labels[i]}"
176
-
177
- # ラベルを追加
178
- label_actor = self.main_window.plotter.add_point_labels(
179
- [pos], [label_text],
180
- point_size=20,
181
- font_size=12,
182
- text_color=colors[i],
183
- always_visible=True
184
- )
185
- self.selection_labels.append(label_actor)
186
-
187
- def clear_atom_labels(self):
188
- """原子ラベルをクリア"""
189
- if hasattr(self, 'selection_labels'):
190
- for label_actor in self.selection_labels:
191
- try:
192
- self.main_window.plotter.remove_actor(label_actor)
193
- except Exception:
194
- pass
195
- self.selection_labels = []
196
-
197
- def clear_selection_labels(self):
198
- """選択ラベルをクリア"""
199
- if hasattr(self, 'selection_labels'):
200
- for label_actor in self.selection_labels:
201
- try:
202
- self.main_window.plotter.remove_actor(label_actor)
203
- except Exception:
204
- pass
205
- self.selection_labels = []
206
-
207
- def add_selection_label(self, atom_idx, label_text):
208
- """選択された原子にラベルを追加"""
209
- if not hasattr(self, 'selection_labels'):
210
- self.selection_labels = []
211
-
212
- # 原子の位置を取得
213
- pos = self.main_window.atom_positions_3d[atom_idx]
214
-
215
- # ラベルを追加
216
- label_actor = self.main_window.plotter.add_point_labels(
217
- [pos], [label_text],
218
- point_size=20,
219
- font_size=12,
220
- text_color='yellow',
221
- always_visible=True
222
- )
223
- self.selection_labels.append(label_actor)
164
+ pairs = [(idx, labels[i]) for i, idx in enumerate(selected_atoms) if idx is not None]
165
+ self.show_atom_labels_for(pairs)
224
166
 
225
167
  def update_display(self):
226
168
  """表示を更新"""
@@ -317,8 +259,8 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
317
259
  new_pos2 = bond_center + direction * half_distance
318
260
 
319
261
  # Get both connected groups
320
- group1_atoms = self.get_connected_group(self.atom1_idx, exclude=self.atom2_idx)
321
- group2_atoms = self.get_connected_group(self.atom2_idx, exclude=self.atom1_idx)
262
+ group1_atoms = get_connected_group(self.mol, self.atom1_idx, exclude=self.atom2_idx)
263
+ group2_atoms = get_connected_group(self.mol, self.atom2_idx, exclude=self.atom1_idx)
322
264
 
323
265
  # Calculate displacements
324
266
  displacement1 = new_pos1 - pos1
@@ -346,7 +288,7 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
346
288
  else:
347
289
  # Move the connected group (default behavior)
348
290
  new_pos2 = pos1 + direction * new_distance
349
- atoms_to_move = self.get_connected_group(self.atom2_idx, exclude=self.atom1_idx)
291
+ atoms_to_move = get_connected_group(self.mol, self.atom2_idx, exclude=self.atom1_idx)
350
292
  displacement = new_pos2 - pos2
351
293
 
352
294
  for atom_idx in atoms_to_move:
@@ -358,23 +300,4 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
358
300
  # Update the 3D view
359
301
  self.main_window.draw_molecule_3d(self.mol)
360
302
 
361
- def get_connected_group(self, start_atom, exclude=None):
362
- """指定された原子から連結されているグループを取得"""
363
- visited = set()
364
- to_visit = [start_atom]
365
-
366
- while to_visit:
367
- current = to_visit.pop()
368
- if current in visited or current == exclude:
369
- continue
370
-
371
- visited.add(current)
372
-
373
- # Get neighboring atoms
374
- atom = self.mol.GetAtomWithIdx(current)
375
- for bond in atom.GetBonds():
376
- other_idx = bond.GetOtherAtomIdx(current)
377
- if other_idx not in visited and other_idx != exclude:
378
- to_visit.append(other_idx)
379
-
380
- return visited
303
+