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.
- {moleditpy-2.5.2 → moleditpy-2.6.1}/PKG-INFO +1 -1
- {moleditpy-2.5.2 → moleditpy-2.6.1}/pyproject.toml +2 -2
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/SOURCES.txt +7 -5
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/main.py +1 -1
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/__init__.py +1 -11
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/about_dialog.py +15 -15
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/align_plane_dialog.py +40 -60
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/alignment_dialog.py +44 -69
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/analysis_window.py +30 -24
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/angle_dialog.py +71 -141
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/atom_item.py +47 -39
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/bond_item.py +67 -58
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/bond_length_dialog.py +66 -135
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/calculation_worker.py +104 -101
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/color_settings_dialog.py +18 -9
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/constants.py +3 -3
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/constrained_optimization_dialog.py +91 -92
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/custom_interactor_style.py +73 -79
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/custom_qt_interactor.py +6 -5
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/dialog3_d_picking_mixin.py +87 -19
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/dihedral_dialog.py +86 -162
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window.py +4 -31
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_app_state.py +48 -74
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_compute.py +53 -89
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_dialog_manager.py +70 -91
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_edit_3d.py +72 -129
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_edit_actions.py +107 -134
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_export.py +111 -134
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_main_init.py +202 -201
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_molecular_parsers.py +74 -87
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_project_io.py +68 -88
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_string_importers.py +20 -41
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_ui_manager.py +42 -61
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_view_3d.py +130 -153
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_view_loaders.py +23 -42
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/mirror_dialog.py +37 -26
- moleditpy-2.6.1/src/moleditpy/modules/mol_geometry.py +164 -0
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/molecular_data.py +12 -12
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/molecule_scene.py +134 -147
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/move_group_dialog.py +112 -107
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/periodic_table_dialog.py +3 -1
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/planarize_dialog.py +15 -30
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_interface.py +13 -23
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_manager.py +54 -55
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_manager_window.py +43 -34
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/settings_dialog.py +102 -238
- moleditpy-2.6.1/src/moleditpy/modules/sip_isdeleted_safe.py +16 -0
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/template_preview_item.py +18 -27
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/template_preview_view.py +9 -9
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/translation_dialog.py +44 -43
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/user_template_dialog.py +115 -103
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/zoomable_view.py +7 -12
- {moleditpy-2.5.2 → moleditpy-2.6.1}/LICENSE +0 -0
- {moleditpy-2.5.2 → moleditpy-2.6.1}/README.md +0 -0
- {moleditpy-2.5.2 → moleditpy-2.6.1}/setup.cfg +0 -0
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/__init__.py +0 -0
- {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/__main__.py +0 -0
- {moleditpy-2.5.2/src/moleditpy/modules → moleditpy-2.6.1/src/moleditpy}/assets/file_icon.ico +0 -0
- {moleditpy-2.5.2/src/moleditpy/modules → moleditpy-2.6.1/src/moleditpy}/assets/icon.icns +0 -0
- {moleditpy-2.5.2/src/moleditpy/modules → moleditpy-2.6.1/src/moleditpy}/assets/icon.ico +0 -0
- {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.
|
|
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.
|
|
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" = ["
|
|
51
|
+
"moleditpy" = ["assets/*"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 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
|
|
@@ -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,
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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,
|
|
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()
|