MoleditPy 3.0.6__tar.gz → 3.1.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.
- {moleditpy-3.0.6 → moleditpy-3.1.0}/PKG-INFO +1 -1
- {moleditpy-3.0.6 → moleditpy-3.1.0}/pyproject.toml +1 -1
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/app_state.py +2 -2
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/dialog_logic.py +0 -4
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/io_logic.py +21 -25
- moleditpy-3.1.0/src/moleditpy/ui/string_importers.py +208 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/user_template_dialog.py +0 -4
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/view_3d_logic.py +3 -2
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/utils/constants.py +1 -1
- moleditpy-3.0.6/src/moleditpy/ui/string_importers.py +0 -257
- {moleditpy-3.0.6 → moleditpy-3.1.0}/LICENSE +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/README.md +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/setup.cfg +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/__init__.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/__main__.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/assets/file_icon.ico +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/assets/icon.icns +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/core/__init__.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/core/mol_geometry.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/core/molecular_data.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/main.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/plugins/__init__.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/plugins/plugin_interface.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/plugins/plugin_manager.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/plugins/plugin_manager_window.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/__init__.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/about_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/align_plane_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/alignment_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/analysis_window.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/angle_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/atom_item.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/base_picking_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/bond_item.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/bond_length_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/calculation_worker.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/color_settings_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/compute_logic.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/custom_interactor_style.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/dihedral_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/edit_3d_logic.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/edit_actions_logic.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/export_logic.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/geometry_base_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/main_window.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/main_window_init.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/mirror_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/molecular_scene_handler.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/molecule_scene.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/move_group_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/planarize_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/settings_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/sip_isdeleted_safe.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/template_preview_item.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/template_preview_view.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/translation_dialog.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/ui_manager.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/ui/zoomable_view.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/utils/__init__.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/utils/default_settings.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy-3.0.6 → moleditpy-3.1.0}/src/moleditpy/utils/system_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.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
|
|
@@ -364,9 +364,9 @@ class StateManager:
|
|
|
364
364
|
# 'Save As' if not PMEPRJ
|
|
365
365
|
file_path = self.host.init_manager.current_file_path
|
|
366
366
|
if not file_path or not file_path.lower().endswith(".pmeprj"):
|
|
367
|
-
self.save_project_as()
|
|
367
|
+
self.host.io_manager.save_project_as()
|
|
368
368
|
else:
|
|
369
|
-
self.save_project()
|
|
369
|
+
self.host.io_manager.save_project()
|
|
370
370
|
return (
|
|
371
371
|
not self.host.state_manager.has_unsaved_changes
|
|
372
372
|
) # Return True only if save was successful
|
|
@@ -200,10 +200,6 @@ class DialogManager:
|
|
|
200
200
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
201
201
|
json.dump(template_data, f, indent=2, ensure_ascii=False)
|
|
202
202
|
|
|
203
|
-
# Mark as saved (no unsaved changes for this operation)
|
|
204
|
-
self.host.state_manager.has_unsaved_changes = False
|
|
205
|
-
self.host.state_manager.update_window_title()
|
|
206
|
-
|
|
207
203
|
QMessageBox.information(
|
|
208
204
|
self.host, "Success", f"Template '{name}' saved successfully."
|
|
209
205
|
)
|
|
@@ -564,10 +564,7 @@ class IOManager:
|
|
|
564
564
|
self.host.statusBar().showMessage(f"Export error: {e}")
|
|
565
565
|
|
|
566
566
|
def load_mol_file(self, file_path: Optional[str] = None) -> None:
|
|
567
|
-
"""
|
|
568
|
-
if not self.host.state_manager.check_unsaved_changes():
|
|
569
|
-
return
|
|
570
|
-
|
|
567
|
+
"""Import a MOL/SDF file and add its contents to the 2D editor."""
|
|
571
568
|
if not file_path:
|
|
572
569
|
default_dir = (
|
|
573
570
|
os.path.dirname(self.host.init_manager.current_file_path)
|
|
@@ -601,11 +598,6 @@ class IOManager:
|
|
|
601
598
|
raise ValueError("Failed to read molecule from file.")
|
|
602
599
|
|
|
603
600
|
Chem.Kekulize(mol)
|
|
604
|
-
self.host.ui_manager.restore_ui_for_editing()
|
|
605
|
-
self.host.edit_actions_manager.clear_2d_editor(push_to_undo=False)
|
|
606
|
-
self.host.view_3d_manager.current_mol = None
|
|
607
|
-
self.host.view_3d_manager.plotter.clear()
|
|
608
|
-
self.host.init_manager.analysis_action.setEnabled(False)
|
|
609
601
|
|
|
610
602
|
if mol.GetNumConformers() == 0:
|
|
611
603
|
AllChem.Compute2DCoords(mol)
|
|
@@ -615,9 +607,22 @@ class IOManager:
|
|
|
615
607
|
AllChem.WedgeMolBonds(mol, conf)
|
|
616
608
|
|
|
617
609
|
SCALE_FACTOR = 50.0
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
610
|
+
existing_atoms = self.host.state_manager.data.atoms
|
|
611
|
+
if existing_atoms:
|
|
612
|
+
max_x = max(
|
|
613
|
+
v["pos"].x() if hasattr(v["pos"], "x") else v["pos"][0]
|
|
614
|
+
for v in existing_atoms.values()
|
|
615
|
+
)
|
|
616
|
+
avg_y = sum(
|
|
617
|
+
v["pos"].y() if hasattr(v["pos"], "y") else v["pos"][1]
|
|
618
|
+
for v in existing_atoms.values()
|
|
619
|
+
) / len(existing_atoms)
|
|
620
|
+
place_center = QPointF(max_x + 80.0, avg_y)
|
|
621
|
+
else:
|
|
622
|
+
place_center = self.host.init_manager.view_2d.mapToScene(
|
|
623
|
+
self.host.init_manager.view_2d.viewport().rect().center()
|
|
624
|
+
)
|
|
625
|
+
|
|
621
626
|
positions = [conf.GetAtomPosition(i) for i in range(mol.GetNumAtoms())]
|
|
622
627
|
mol_center_x = (
|
|
623
628
|
sum(p.x for p in positions) / len(positions) if positions else 0.0
|
|
@@ -630,8 +635,8 @@ class IOManager:
|
|
|
630
635
|
for i in range(mol.GetNumAtoms()):
|
|
631
636
|
atom = mol.GetAtomWithIdx(i)
|
|
632
637
|
pos = conf.GetAtomPosition(i)
|
|
633
|
-
scene_x = ((pos.x - mol_center_x) * SCALE_FACTOR) +
|
|
634
|
-
scene_y = (-(pos.y - mol_center_y) * SCALE_FACTOR) +
|
|
638
|
+
scene_x = ((pos.x - mol_center_x) * SCALE_FACTOR) + place_center.x()
|
|
639
|
+
scene_y = (-(pos.y - mol_center_y) * SCALE_FACTOR) + place_center.y()
|
|
635
640
|
atom_id = self.host.init_manager.scene.create_atom(
|
|
636
641
|
atom.GetSymbol(),
|
|
637
642
|
QPointF(scene_x, scene_y),
|
|
@@ -662,18 +667,9 @@ class IOManager:
|
|
|
662
667
|
bond_stereo=stereo,
|
|
663
668
|
)
|
|
664
669
|
|
|
665
|
-
self.host.statusBar().showMessage(f"Successfully
|
|
666
|
-
self.host.state_manager.reset_undo_stack()
|
|
667
|
-
self.host.init_manager.current_file_path = file_path
|
|
668
|
-
self.host.state_manager.has_unsaved_changes = False
|
|
669
|
-
self.host.state_manager.update_window_title()
|
|
670
|
+
self.host.statusBar().showMessage(f"Successfully imported {file_path}")
|
|
670
671
|
self.host.init_manager.scene.update_all_items()
|
|
671
|
-
|
|
672
|
-
# Reset camera/zoom after drawing
|
|
673
|
-
QTimer.singleShot(
|
|
674
|
-
50, lambda: self.host.view_3d_manager.plotter.view_isometric()
|
|
675
|
-
)
|
|
676
|
-
QTimer.singleShot(100, lambda: self.host.view_3d_manager.plotter.render())
|
|
672
|
+
self.host.edit_actions_manager.push_undo_state()
|
|
677
673
|
QTimer.singleShot(100, self.host.view_3d_manager.fit_to_view)
|
|
678
674
|
except Exception as e:
|
|
679
675
|
self.host.statusBar().showMessage(f"Error loading file: {e}")
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MoleditPy — A Python-based molecular editing software
|
|
6
|
+
|
|
7
|
+
Author: Hiromichi Yokoyama
|
|
8
|
+
License: GPL-3.0 license
|
|
9
|
+
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
+
DOI: 10.5281/zenodo.17268532
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
# RDKit imports (explicit to satisfy flake8 and used features)
|
|
16
|
+
from rdkit import Chem
|
|
17
|
+
from rdkit.Chem import AllChem
|
|
18
|
+
|
|
19
|
+
# PyQt6 Modules
|
|
20
|
+
from PyQt6.QtCore import QPointF, QTimer
|
|
21
|
+
from PyQt6.QtWidgets import QInputDialog
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from PyQt6 import sip as _sip # type: ignore
|
|
25
|
+
|
|
26
|
+
_sip_isdeleted = getattr(_sip, "isdeleted", None)
|
|
27
|
+
except ImportError:
|
|
28
|
+
_sip = None
|
|
29
|
+
_sip_isdeleted = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --- Classes ---
|
|
33
|
+
class StringImporterManager:
|
|
34
|
+
"""Mixin for string-based molecular input (SMILES, InChI)."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, host):
|
|
37
|
+
self.host = host
|
|
38
|
+
|
|
39
|
+
def import_smiles_dialog(self) -> None:
|
|
40
|
+
"""Dialog for SMILES input."""
|
|
41
|
+
smiles, ok = QInputDialog.getText(
|
|
42
|
+
self.host, "Import SMILES", "Enter SMILES string:"
|
|
43
|
+
)
|
|
44
|
+
if ok and smiles:
|
|
45
|
+
self.load_from_smiles(smiles)
|
|
46
|
+
|
|
47
|
+
def import_inchi_dialog(self) -> None:
|
|
48
|
+
"""Dialog for InChI input."""
|
|
49
|
+
inchi, ok = QInputDialog.getText(
|
|
50
|
+
self.host, "Import InChI", "Enter InChI string:"
|
|
51
|
+
)
|
|
52
|
+
if ok and inchi:
|
|
53
|
+
self.load_from_inchi(inchi)
|
|
54
|
+
|
|
55
|
+
def _placement_center(self) -> QPointF:
|
|
56
|
+
"""Return the scene point where the next imported molecule should be centered.
|
|
57
|
+
|
|
58
|
+
If the editor already contains atoms, offset to the right of the
|
|
59
|
+
rightmost atom so the new fragment does not overlap. Otherwise use
|
|
60
|
+
the current viewport center.
|
|
61
|
+
"""
|
|
62
|
+
existing = self.host.state_manager.data.atoms
|
|
63
|
+
if existing:
|
|
64
|
+
|
|
65
|
+
def _x(v):
|
|
66
|
+
return v["pos"].x() if hasattr(v["pos"], "x") else v["pos"][0]
|
|
67
|
+
|
|
68
|
+
def _y(v):
|
|
69
|
+
return v["pos"].y() if hasattr(v["pos"], "y") else v["pos"][1]
|
|
70
|
+
|
|
71
|
+
max_x = max(_x(v) for v in existing.values())
|
|
72
|
+
avg_y = sum(_y(v) for v in existing.values()) / len(existing)
|
|
73
|
+
return QPointF(max_x + 80.0, avg_y)
|
|
74
|
+
|
|
75
|
+
return self.host.init_manager.view_2d.mapToScene(
|
|
76
|
+
self.host.init_manager.view_2d.viewport().rect().center()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def _place_mol_bonds(self, mol, rdkit_idx_to_my_id: dict) -> None:
|
|
80
|
+
"""Create bonds in the 2D scene from an RDKit molecule."""
|
|
81
|
+
for bond in mol.GetBonds():
|
|
82
|
+
b_idx, e_idx = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
|
|
83
|
+
b_type = bond.GetBondTypeAsDouble()
|
|
84
|
+
b_dir = bond.GetBondDir()
|
|
85
|
+
stereo = 0
|
|
86
|
+
if b_dir == Chem.BondDir.BEGINWEDGE:
|
|
87
|
+
stereo = 1
|
|
88
|
+
elif b_dir == Chem.BondDir.BEGINDASH:
|
|
89
|
+
stereo = 2
|
|
90
|
+
if bond.GetBondType() == Chem.BondType.DOUBLE:
|
|
91
|
+
if bond.GetStereo() == Chem.BondStereo.STEREOZ:
|
|
92
|
+
stereo = 3
|
|
93
|
+
elif bond.GetStereo() == Chem.BondStereo.STEREOE:
|
|
94
|
+
stereo = 4
|
|
95
|
+
|
|
96
|
+
if b_idx in rdkit_idx_to_my_id and e_idx in rdkit_idx_to_my_id:
|
|
97
|
+
a1_id = rdkit_idx_to_my_id[b_idx]
|
|
98
|
+
a2_id = rdkit_idx_to_my_id[e_idx]
|
|
99
|
+
a1_item = self.host.state_manager.data.atoms[a1_id]["item"]
|
|
100
|
+
a2_item = self.host.state_manager.data.atoms[a2_id]["item"]
|
|
101
|
+
self.host.init_manager.scene.create_bond(
|
|
102
|
+
a1_item, a2_item, bond_order=int(b_type), bond_stereo=stereo
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _place_mol_atoms(self, mol, conf, place_center: QPointF) -> dict:
|
|
106
|
+
"""Create atoms in the 2D scene and return rdkit_idx → atom_id mapping."""
|
|
107
|
+
SCALE_FACTOR = 50.0
|
|
108
|
+
positions = [conf.GetAtomPosition(i) for i in range(mol.GetNumAtoms())]
|
|
109
|
+
mol_center_x = (
|
|
110
|
+
sum(p.x for p in positions) / len(positions) if positions else 0.0
|
|
111
|
+
)
|
|
112
|
+
mol_center_y = (
|
|
113
|
+
sum(p.y for p in positions) / len(positions) if positions else 0.0
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
rdkit_idx_to_my_id = {}
|
|
117
|
+
for i in range(mol.GetNumAtoms()):
|
|
118
|
+
atom = mol.GetAtomWithIdx(i)
|
|
119
|
+
pos = conf.GetAtomPosition(i)
|
|
120
|
+
scene_x = ((pos.x - mol_center_x) * SCALE_FACTOR) + place_center.x()
|
|
121
|
+
scene_y = (-(pos.y - mol_center_y) * SCALE_FACTOR) + place_center.y()
|
|
122
|
+
atom_id = self.host.init_manager.scene.create_atom(
|
|
123
|
+
atom.GetSymbol(),
|
|
124
|
+
QPointF(scene_x, scene_y),
|
|
125
|
+
charge=atom.GetFormalCharge(),
|
|
126
|
+
)
|
|
127
|
+
rdkit_idx_to_my_id[i] = atom_id
|
|
128
|
+
return rdkit_idx_to_my_id
|
|
129
|
+
|
|
130
|
+
def load_from_smiles(self, smiles_string: str) -> None:
|
|
131
|
+
"""Add a molecule from a SMILES string to the 2D editor."""
|
|
132
|
+
cleaned_smiles = smiles_string.strip()
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
mol = Chem.MolFromSmiles(cleaned_smiles)
|
|
136
|
+
if mol is None:
|
|
137
|
+
if not cleaned_smiles:
|
|
138
|
+
raise ValueError("SMILES string was empty.")
|
|
139
|
+
raise ValueError("Invalid SMILES string.")
|
|
140
|
+
|
|
141
|
+
AllChem.Compute2DCoords(mol)
|
|
142
|
+
Chem.Kekulize(mol)
|
|
143
|
+
AllChem.AssignStereochemistry(mol, cleanIt=True, force=True)
|
|
144
|
+
conf = mol.GetConformer()
|
|
145
|
+
AllChem.WedgeMolBonds(mol, conf)
|
|
146
|
+
except ValueError as e:
|
|
147
|
+
self.host.statusBar().showMessage(f"Invalid SMILES: {e}")
|
|
148
|
+
return
|
|
149
|
+
except (RuntimeError, TypeError, AttributeError) as e:
|
|
150
|
+
self.host.statusBar().showMessage(f"Error parsing SMILES: {e}")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
place_center = self._placement_center()
|
|
155
|
+
rdkit_idx_to_my_id = self._place_mol_atoms(
|
|
156
|
+
mol, mol.GetConformer(), place_center
|
|
157
|
+
)
|
|
158
|
+
self._place_mol_bonds(mol, rdkit_idx_to_my_id)
|
|
159
|
+
|
|
160
|
+
self.host.statusBar().showMessage("Successfully loaded from SMILES.")
|
|
161
|
+
self.host.init_manager.scene.update_all_items()
|
|
162
|
+
self.host.edit_actions_manager.push_undo_state()
|
|
163
|
+
QTimer.singleShot(0, self.host.view_3d_manager.fit_to_view)
|
|
164
|
+
|
|
165
|
+
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
166
|
+
self.host.statusBar().showMessage(f"Error loading from SMILES: {e}")
|
|
167
|
+
|
|
168
|
+
def load_from_inchi(self, inchi_string: str) -> None:
|
|
169
|
+
"""Add a molecule from an InChI string to the 2D editor."""
|
|
170
|
+
cleaned_inchi = inchi_string.strip()
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
mol = Chem.MolFromInchi(cleaned_inchi)
|
|
174
|
+
if mol is None:
|
|
175
|
+
if not cleaned_inchi:
|
|
176
|
+
raise ValueError("InChI string was empty.")
|
|
177
|
+
raise ValueError("Invalid InChI string.")
|
|
178
|
+
|
|
179
|
+
AllChem.Compute2DCoords(mol)
|
|
180
|
+
Chem.Kekulize(mol)
|
|
181
|
+
AllChem.AssignStereochemistry(mol, cleanIt=True, force=True)
|
|
182
|
+
conf = mol.GetConformer()
|
|
183
|
+
AllChem.WedgeMolBonds(mol, conf)
|
|
184
|
+
except ValueError as e:
|
|
185
|
+
self.host.statusBar().showMessage(f"Invalid InChI: {e}")
|
|
186
|
+
return
|
|
187
|
+
except (RuntimeError, TypeError, AttributeError) as e:
|
|
188
|
+
self.host.statusBar().showMessage(f"Error parsing InChI: {e}")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
place_center = self._placement_center()
|
|
193
|
+
rdkit_idx_to_my_id = self._place_mol_atoms(
|
|
194
|
+
mol, mol.GetConformer(), place_center
|
|
195
|
+
)
|
|
196
|
+
self._place_mol_bonds(mol, rdkit_idx_to_my_id)
|
|
197
|
+
|
|
198
|
+
self.host.statusBar().showMessage("Successfully loaded from InChI.")
|
|
199
|
+
self.host.init_manager.scene.update_all_items()
|
|
200
|
+
self.host.edit_actions_manager.push_undo_state()
|
|
201
|
+
QTimer.singleShot(0, self.host.view_3d_manager.fit_to_view)
|
|
202
|
+
|
|
203
|
+
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
204
|
+
self.host.statusBar().showMessage(f"Error loading from InChI: {e}")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Backward-compat aliases
|
|
208
|
+
MainWindowStringImporters = StringImporterManager
|
|
@@ -634,10 +634,6 @@ class UserTemplateDialog(QDialog):
|
|
|
634
634
|
return
|
|
635
635
|
|
|
636
636
|
if self.save_template_file(filepath, template_data):
|
|
637
|
-
# Mark main window as saved
|
|
638
|
-
self.main_window.state_manager.has_unsaved_changes = False
|
|
639
|
-
self.main_window.state_manager.update_window_title()
|
|
640
|
-
|
|
641
637
|
QMessageBox.information(
|
|
642
638
|
self, "Success", f"Template '{name}' saved successfully."
|
|
643
639
|
)
|
|
@@ -282,8 +282,9 @@ class View3DManager:
|
|
|
282
282
|
self.host.view_3d_manager.plotter.camera_position = camera_state
|
|
283
283
|
|
|
284
284
|
# Update projection mode and force render
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
proj_mode = self.host.init_manager.settings.get(
|
|
286
|
+
"projection_mode", "Perspective"
|
|
287
|
+
)
|
|
287
288
|
if hasattr(self.host.view_3d_manager.plotter, "renderer") and hasattr(
|
|
288
289
|
self.host.view_3d_manager.plotter.renderer, "GetActiveCamera"
|
|
289
290
|
):
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
MoleditPy — A Python-based molecular editing software
|
|
6
|
-
|
|
7
|
-
Author: Hiromichi Yokoyama
|
|
8
|
-
License: GPL-3.0 license
|
|
9
|
-
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
-
DOI: 10.5281/zenodo.17268532
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
# RDKit imports (explicit to satisfy flake8 and used features)
|
|
16
|
-
from rdkit import Chem
|
|
17
|
-
from rdkit.Chem import AllChem
|
|
18
|
-
|
|
19
|
-
# PyQt6 Modules
|
|
20
|
-
from PyQt6.QtCore import QPointF, QTimer
|
|
21
|
-
from PyQt6.QtWidgets import QInputDialog
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
from PyQt6 import sip as _sip # type: ignore
|
|
25
|
-
|
|
26
|
-
_sip_isdeleted = getattr(_sip, "isdeleted", None)
|
|
27
|
-
except ImportError:
|
|
28
|
-
_sip = None
|
|
29
|
-
_sip_isdeleted = None
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# --- Classes ---
|
|
33
|
-
class StringImporterManager:
|
|
34
|
-
"""Mixin for string-based molecular input (SMILES, InChI)."""
|
|
35
|
-
|
|
36
|
-
def __init__(self, host):
|
|
37
|
-
self.host = host
|
|
38
|
-
|
|
39
|
-
def import_smiles_dialog(self) -> None:
|
|
40
|
-
"""Dialog for SMILES input."""
|
|
41
|
-
smiles, ok = QInputDialog.getText(
|
|
42
|
-
self.host, "Import SMILES", "Enter SMILES string:"
|
|
43
|
-
)
|
|
44
|
-
if ok and smiles:
|
|
45
|
-
self.load_from_smiles(smiles)
|
|
46
|
-
|
|
47
|
-
def import_inchi_dialog(self) -> None:
|
|
48
|
-
"""Dialog for InChI input."""
|
|
49
|
-
inchi, ok = QInputDialog.getText(
|
|
50
|
-
self.host, "Import InChI", "Enter InChI string:"
|
|
51
|
-
)
|
|
52
|
-
if ok and inchi:
|
|
53
|
-
self.load_from_inchi(inchi)
|
|
54
|
-
|
|
55
|
-
def load_from_smiles(self, smiles_string: str) -> None:
|
|
56
|
-
"""Load molecule from SMILES string to 2D editor."""
|
|
57
|
-
if not self.host.state_manager.check_unsaved_changes():
|
|
58
|
-
return # User cancelled
|
|
59
|
-
|
|
60
|
-
cleaned_smiles = smiles_string.strip()
|
|
61
|
-
|
|
62
|
-
try:
|
|
63
|
-
mol = Chem.MolFromSmiles(cleaned_smiles)
|
|
64
|
-
if mol is None:
|
|
65
|
-
if not cleaned_smiles:
|
|
66
|
-
raise ValueError("SMILES string was empty.")
|
|
67
|
-
raise ValueError("Invalid SMILES string.")
|
|
68
|
-
|
|
69
|
-
AllChem.Compute2DCoords(mol)
|
|
70
|
-
Chem.Kekulize(mol)
|
|
71
|
-
|
|
72
|
-
AllChem.AssignStereochemistry(mol, cleanIt=True, force=True)
|
|
73
|
-
conf = mol.GetConformer()
|
|
74
|
-
AllChem.WedgeMolBonds(mol, conf)
|
|
75
|
-
except ValueError as e:
|
|
76
|
-
self.host.statusBar().showMessage(f"Invalid SMILES: {e}")
|
|
77
|
-
return
|
|
78
|
-
except (RuntimeError, TypeError, AttributeError) as e:
|
|
79
|
-
self.host.statusBar().showMessage(f"Error parsing SMILES: {e}")
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
try:
|
|
83
|
-
self.host.ui_manager.restore_ui_for_editing()
|
|
84
|
-
self.host.edit_actions_manager.clear_2d_editor(push_to_undo=False)
|
|
85
|
-
self.host.view_3d_manager.current_mol = None
|
|
86
|
-
self.host.view_3d_manager.plotter.clear()
|
|
87
|
-
self.host.init_manager.analysis_action.setEnabled(False)
|
|
88
|
-
|
|
89
|
-
conf = mol.GetConformer()
|
|
90
|
-
SCALE_FACTOR = 50.0
|
|
91
|
-
|
|
92
|
-
view_center = self.host.init_manager.view_2d.mapToScene(
|
|
93
|
-
self.host.init_manager.view_2d.viewport().rect().center()
|
|
94
|
-
)
|
|
95
|
-
positions = [conf.GetAtomPosition(i) for i in range(mol.GetNumAtoms())]
|
|
96
|
-
mol_center_x = (
|
|
97
|
-
sum(p.x for p in positions) / len(positions) if positions else 0.0
|
|
98
|
-
)
|
|
99
|
-
mol_center_y = (
|
|
100
|
-
sum(p.y for p in positions) / len(positions) if positions else 0.0
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
rdkit_idx_to_my_id = {}
|
|
104
|
-
for i in range(mol.GetNumAtoms()):
|
|
105
|
-
atom = mol.GetAtomWithIdx(i)
|
|
106
|
-
pos = conf.GetAtomPosition(i)
|
|
107
|
-
charge = atom.GetFormalCharge()
|
|
108
|
-
|
|
109
|
-
relative_x = pos.x - mol_center_x
|
|
110
|
-
relative_y = pos.y - mol_center_y
|
|
111
|
-
|
|
112
|
-
scene_x = (relative_x * SCALE_FACTOR) + view_center.x()
|
|
113
|
-
scene_y = (-relative_y * SCALE_FACTOR) + view_center.y()
|
|
114
|
-
|
|
115
|
-
atom_id = self.host.init_manager.scene.create_atom(
|
|
116
|
-
atom.GetSymbol(), QPointF(scene_x, scene_y), charge=charge
|
|
117
|
-
)
|
|
118
|
-
rdkit_idx_to_my_id[i] = atom_id
|
|
119
|
-
|
|
120
|
-
for bond in mol.GetBonds():
|
|
121
|
-
b_idx, e_idx = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
|
|
122
|
-
b_type = bond.GetBondTypeAsDouble()
|
|
123
|
-
b_dir = bond.GetBondDir()
|
|
124
|
-
stereo = 0
|
|
125
|
-
# Single bond stereo
|
|
126
|
-
if b_dir == Chem.BondDir.BEGINWEDGE:
|
|
127
|
-
stereo = 1 # Wedge
|
|
128
|
-
elif b_dir == Chem.BondDir.BEGINDASH:
|
|
129
|
-
stereo = 2 # Dash
|
|
130
|
-
# Double bond E/Z
|
|
131
|
-
if bond.GetBondType() == Chem.BondType.DOUBLE:
|
|
132
|
-
if bond.GetStereo() == Chem.BondStereo.STEREOZ:
|
|
133
|
-
stereo = 3 # Z
|
|
134
|
-
elif bond.GetStereo() == Chem.BondStereo.STEREOE:
|
|
135
|
-
stereo = 4 # E
|
|
136
|
-
|
|
137
|
-
if b_idx in rdkit_idx_to_my_id and e_idx in rdkit_idx_to_my_id:
|
|
138
|
-
a1_id, a2_id = rdkit_idx_to_my_id[b_idx], rdkit_idx_to_my_id[e_idx]
|
|
139
|
-
a1_item = self.host.state_manager.data.atoms[a1_id]["item"]
|
|
140
|
-
a2_item = self.host.state_manager.data.atoms[a2_id]["item"]
|
|
141
|
-
self.host.init_manager.scene.create_bond(
|
|
142
|
-
a1_item, a2_item, bond_order=int(b_type), bond_stereo=stereo
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
self.host.statusBar().showMessage("Successfully loaded from SMILES.")
|
|
146
|
-
self.host.init_manager.scene.update_all_items()
|
|
147
|
-
self.host.state_manager.reset_undo_stack()
|
|
148
|
-
self.host.state_manager.has_unsaved_changes = False
|
|
149
|
-
self.host.state_manager.update_window_title()
|
|
150
|
-
QTimer.singleShot(0, self.host.view_3d_manager.fit_to_view)
|
|
151
|
-
|
|
152
|
-
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
153
|
-
self.host.statusBar().showMessage(f"Error loading from SMILES: {e}")
|
|
154
|
-
|
|
155
|
-
def load_from_inchi(self, inchi_string: str) -> None:
|
|
156
|
-
"""Load molecule from InChI string to 2D editor."""
|
|
157
|
-
if not self.host.state_manager.check_unsaved_changes():
|
|
158
|
-
return # User cancelled
|
|
159
|
-
|
|
160
|
-
cleaned_inchi = inchi_string.strip()
|
|
161
|
-
|
|
162
|
-
try:
|
|
163
|
-
mol = Chem.MolFromInchi(cleaned_inchi)
|
|
164
|
-
if mol is None:
|
|
165
|
-
if not cleaned_inchi:
|
|
166
|
-
raise ValueError("InChI string was empty.")
|
|
167
|
-
raise ValueError("Invalid InChI string.")
|
|
168
|
-
|
|
169
|
-
AllChem.Compute2DCoords(mol)
|
|
170
|
-
Chem.Kekulize(mol)
|
|
171
|
-
|
|
172
|
-
AllChem.AssignStereochemistry(mol, cleanIt=True, force=True)
|
|
173
|
-
conf = mol.GetConformer()
|
|
174
|
-
AllChem.WedgeMolBonds(mol, conf)
|
|
175
|
-
except ValueError as e:
|
|
176
|
-
self.host.statusBar().showMessage(f"Invalid InChI: {e}")
|
|
177
|
-
return
|
|
178
|
-
except (RuntimeError, TypeError, AttributeError) as e:
|
|
179
|
-
self.host.statusBar().showMessage(f"Error parsing InChI: {e}")
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
try:
|
|
183
|
-
self.host.ui_manager.restore_ui_for_editing()
|
|
184
|
-
self.host.edit_actions_manager.clear_2d_editor(push_to_undo=False)
|
|
185
|
-
self.host.view_3d_manager.current_mol = None
|
|
186
|
-
self.host.view_3d_manager.plotter.clear()
|
|
187
|
-
self.host.init_manager.analysis_action.setEnabled(False)
|
|
188
|
-
|
|
189
|
-
conf = mol.GetConformer()
|
|
190
|
-
SCALE_FACTOR = 50.0
|
|
191
|
-
|
|
192
|
-
view_center = self.host.init_manager.view_2d.mapToScene(
|
|
193
|
-
self.host.init_manager.view_2d.viewport().rect().center()
|
|
194
|
-
)
|
|
195
|
-
positions = [conf.GetAtomPosition(i) for i in range(mol.GetNumAtoms())]
|
|
196
|
-
mol_center_x = (
|
|
197
|
-
sum(p.x for p in positions) / len(positions) if positions else 0.0
|
|
198
|
-
)
|
|
199
|
-
mol_center_y = (
|
|
200
|
-
sum(p.y for p in positions) / len(positions) if positions else 0.0
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
rdkit_idx_to_my_id = {}
|
|
204
|
-
for i in range(mol.GetNumAtoms()):
|
|
205
|
-
atom = mol.GetAtomWithIdx(i)
|
|
206
|
-
pos = conf.GetAtomPosition(i)
|
|
207
|
-
charge = atom.GetFormalCharge()
|
|
208
|
-
|
|
209
|
-
relative_x = pos.x - mol_center_x
|
|
210
|
-
relative_y = pos.y - mol_center_y
|
|
211
|
-
|
|
212
|
-
scene_x = (relative_x * SCALE_FACTOR) + view_center.x()
|
|
213
|
-
scene_y = (-relative_y * SCALE_FACTOR) + view_center.y()
|
|
214
|
-
|
|
215
|
-
atom_id = self.host.init_manager.scene.create_atom(
|
|
216
|
-
atom.GetSymbol(), QPointF(scene_x, scene_y), charge=charge
|
|
217
|
-
)
|
|
218
|
-
rdkit_idx_to_my_id[i] = atom_id
|
|
219
|
-
|
|
220
|
-
for bond in mol.GetBonds():
|
|
221
|
-
b_idx, e_idx = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
|
|
222
|
-
b_type = bond.GetBondTypeAsDouble()
|
|
223
|
-
b_dir = bond.GetBondDir()
|
|
224
|
-
stereo = 0
|
|
225
|
-
# Single bond stereo
|
|
226
|
-
if b_dir == Chem.BondDir.BEGINWEDGE:
|
|
227
|
-
stereo = 1 # Wedge
|
|
228
|
-
elif b_dir == Chem.BondDir.BEGINDASH:
|
|
229
|
-
stereo = 2 # Dash
|
|
230
|
-
# Double bond E/Z
|
|
231
|
-
if bond.GetBondType() == Chem.BondType.DOUBLE:
|
|
232
|
-
if bond.GetStereo() == Chem.BondStereo.STEREOZ:
|
|
233
|
-
stereo = 3 # Z
|
|
234
|
-
elif bond.GetStereo() == Chem.BondStereo.STEREOE:
|
|
235
|
-
stereo = 4 # E
|
|
236
|
-
|
|
237
|
-
if b_idx in rdkit_idx_to_my_id and e_idx in rdkit_idx_to_my_id:
|
|
238
|
-
a1_id, a2_id = rdkit_idx_to_my_id[b_idx], rdkit_idx_to_my_id[e_idx]
|
|
239
|
-
a1_item = self.host.state_manager.data.atoms[a1_id]["item"]
|
|
240
|
-
a2_item = self.host.state_manager.data.atoms[a2_id]["item"]
|
|
241
|
-
self.host.init_manager.scene.create_bond(
|
|
242
|
-
a1_item, a2_item, bond_order=int(b_type), bond_stereo=stereo
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
self.host.statusBar().showMessage("Successfully loaded from InChI.")
|
|
246
|
-
self.host.init_manager.scene.update_all_items()
|
|
247
|
-
self.host.state_manager.reset_undo_stack()
|
|
248
|
-
self.host.state_manager.has_unsaved_changes = False
|
|
249
|
-
self.host.state_manager.update_window_title()
|
|
250
|
-
QTimer.singleShot(0, self.host.view_3d_manager.fit_to_view)
|
|
251
|
-
|
|
252
|
-
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
253
|
-
self.host.statusBar().showMessage(f"Error loading from InChI: {e}")
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
# Backward-compat aliases
|
|
257
|
-
MainWindowStringImporters = StringImporterManager
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|