MoleditPy-linux 4.7.2__tar.gz → 4.8.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_linux-4.7.2 → moleditpy_linux-4.8.1}/PKG-INFO +1 -1
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/pyproject.toml +1 -1
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/MoleditPy_linux.egg-info/SOURCES.txt +1 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/plugins/plugin_interface.py +12 -2
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/plugins/plugin_manager.py +14 -2
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/about_dialog.py +4 -2
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/dialog_logic.py +2 -20
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/edit_actions_logic.py +4 -2
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/molecular_scene_handler.py +177 -19
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/plugin_menu_manager.py +231 -60
- moleditpy_linux-4.8.1/src/moleditpy_linux/ui/preview_molecule.py +204 -0
- moleditpy_linux-4.8.1/src/moleditpy_linux/ui/template_preview_item.py +422 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/template_preview_view.py +2 -2
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/ui_manager.py +18 -27
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/user_template_dialog.py +70 -198
- moleditpy_linux-4.7.2/src/moleditpy_linux/ui/template_preview_item.py +0 -251
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/LICENSE +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/README.md +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/setup.cfg +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/__init__.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/core/__init__.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/core/mol_geometry.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/core/molecular_data.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/main.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/plugins/__init__.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/__init__.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/analysis_window.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/app_state.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/atom_item.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/atom_picking.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/bond_item.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/chain_mixin.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/compute_logic.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/export_logic.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/io_logic.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/main_window.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/main_window_init.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/string_importers.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/utils/__init__.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/utils/constants.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/utils/default_settings.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/utils/hit_radius.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/utils/suppress_log.py +0 -0
- {moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/utils/system_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.8.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.8.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
|
|
@@ -58,6 +58,7 @@ src/moleditpy_linux/ui/move_selected_atoms_dialog.py
|
|
|
58
58
|
src/moleditpy_linux/ui/periodic_table_dialog.py
|
|
59
59
|
src/moleditpy_linux/ui/planarize_dialog.py
|
|
60
60
|
src/moleditpy_linux/ui/plugin_menu_manager.py
|
|
61
|
+
src/moleditpy_linux/ui/preview_molecule.py
|
|
61
62
|
src/moleditpy_linux/ui/settings_dialog.py
|
|
62
63
|
src/moleditpy_linux/ui/string_importers.py
|
|
63
64
|
src/moleditpy_linux/ui/template_preview_item.py
|
{moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/plugins/plugin_interface.py
RENAMED
|
@@ -31,6 +31,7 @@ class PluginContext:
|
|
|
31
31
|
text: Optional[str] = None,
|
|
32
32
|
icon: Optional[str] = None,
|
|
33
33
|
shortcut: Optional[str] = None,
|
|
34
|
+
pin: Optional[str] = None,
|
|
34
35
|
) -> None:
|
|
35
36
|
"""
|
|
36
37
|
Register a menu action.
|
|
@@ -41,9 +42,16 @@ class PluginContext:
|
|
|
41
42
|
text: Label for the action (defaults to last part of path if None).
|
|
42
43
|
icon: Path to icon or icon name (optional).
|
|
43
44
|
shortcut: Keyboard shortcut (optional).
|
|
45
|
+
pin: Placement request. ``"header"`` puts a direct "Plugin/<entry>"
|
|
46
|
+
registration next to "Plugin Manager..." above the first
|
|
47
|
+
divider, for plugins that manage the plugin system itself.
|
|
48
|
+
Anything else is ignored, so unknown values stay forward
|
|
49
|
+
compatible. Added in 4.8.1 — passing it to an older MoleditPy
|
|
50
|
+
raises TypeError, so guard the call if your plugin supports
|
|
51
|
+
both (see PLUGIN_DEVELOPMENT_MANUAL_V4.md).
|
|
44
52
|
"""
|
|
45
53
|
self._manager.register_menu_action(
|
|
46
|
-
self._plugin_name, path, callback, text, icon, shortcut
|
|
54
|
+
self._plugin_name, path, callback, text, icon, shortcut, pin
|
|
47
55
|
)
|
|
48
56
|
|
|
49
57
|
def register_menu_action(
|
|
@@ -72,6 +80,7 @@ class PluginContext:
|
|
|
72
80
|
text: Optional[str] = None,
|
|
73
81
|
icon: Optional[str] = None,
|
|
74
82
|
shortcut: Optional[str] = None,
|
|
83
|
+
pin: Optional[str] = None,
|
|
75
84
|
) -> None:
|
|
76
85
|
"""
|
|
77
86
|
Register an action nested inside the Plugin menu.
|
|
@@ -87,9 +96,10 @@ class PluginContext:
|
|
|
87
96
|
text: Label override (defaults to last part of path).
|
|
88
97
|
icon: Path to icon (optional).
|
|
89
98
|
shortcut: Keyboard shortcut (optional).
|
|
99
|
+
pin: Placement request; see add_menu_action.
|
|
90
100
|
"""
|
|
91
101
|
full_path = f"Plugin/{path.lstrip('/')}"
|
|
92
|
-
self.add_menu_action(full_path, callback, text, icon, shortcut)
|
|
102
|
+
self.add_menu_action(full_path, callback, text, icon, shortcut, pin)
|
|
93
103
|
|
|
94
104
|
def add_toolbar_action(
|
|
95
105
|
self,
|
{moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/plugins/plugin_manager.py
RENAMED
|
@@ -358,7 +358,13 @@ class PluginManager:
|
|
|
358
358
|
plugin_desc = getattr(
|
|
359
359
|
module, "PLUGIN_DESCRIPTION", getattr(module, "__doc__", "")
|
|
360
360
|
)
|
|
361
|
-
|
|
361
|
+
# The folder the user filed the plugin under wins: that
|
|
362
|
+
# placement is a deliberate, user-controlled ordering. A
|
|
363
|
+
# plugin's own PLUGIN_CATEGORY only applies when the file sits
|
|
364
|
+
# at the plugins root, where there is no folder to honour.
|
|
365
|
+
plugin_category = category or str(
|
|
366
|
+
getattr(module, "PLUGIN_CATEGORY", "") or ""
|
|
367
|
+
)
|
|
362
368
|
|
|
363
369
|
# Additional cleanup for docstring (strip whitespace)
|
|
364
370
|
if plugin_desc is None:
|
|
@@ -445,8 +451,13 @@ class PluginManager:
|
|
|
445
451
|
text: str,
|
|
446
452
|
icon: str,
|
|
447
453
|
shortcut: str,
|
|
454
|
+
pin: Optional[str] = None,
|
|
448
455
|
) -> None:
|
|
449
|
-
"""Register a plugin menu action with its path, callback, and display metadata.
|
|
456
|
+
"""Register a plugin menu action with its path, callback, and display metadata.
|
|
457
|
+
|
|
458
|
+
``pin`` is optional so a V3-era caller reaching this method directly
|
|
459
|
+
keeps working.
|
|
460
|
+
"""
|
|
450
461
|
self.menu_actions.append(
|
|
451
462
|
{
|
|
452
463
|
"plugin": plugin_name,
|
|
@@ -455,6 +466,7 @@ class PluginManager:
|
|
|
455
466
|
"text": text,
|
|
456
467
|
"icon": icon,
|
|
457
468
|
"shortcut": shortcut,
|
|
469
|
+
"pin": pin,
|
|
458
470
|
}
|
|
459
471
|
)
|
|
460
472
|
|
|
@@ -100,15 +100,16 @@ class AboutDialog(QDialog):
|
|
|
100
100
|
layout.addLayout(button_layout)
|
|
101
101
|
|
|
102
102
|
def image_clicked(self, event: QMouseEvent) -> None:
|
|
103
|
-
"""
|
|
103
|
+
"""Clear all, load bipyrimidine from SMILES, and build it in 3D."""
|
|
104
104
|
# Clear the current scene
|
|
105
105
|
self.main_window.edit_actions_manager.clear_all()
|
|
106
106
|
|
|
107
107
|
bipyrimidine_smiles = "C1=CN=C(N=C1)C2=NC=CC=N2"
|
|
108
108
|
self.main_window.string_importer_manager.load_from_smiles(bipyrimidine_smiles)
|
|
109
109
|
|
|
110
|
-
# Close the dialog
|
|
110
|
+
# Close the dialog first: the conversion puts up its own
|
|
111
111
|
self.accept()
|
|
112
|
+
self.main_window.compute_manager.trigger_conversion()
|
|
112
113
|
|
|
113
114
|
def image_mouse_press_event(self, event: QMouseEvent) -> None:
|
|
114
115
|
"""Handle mouse press on the image: trigger easter egg only for right-click."""
|
|
@@ -118,6 +119,7 @@ class AboutDialog(QDialog):
|
|
|
118
119
|
else:
|
|
119
120
|
event.ignore()
|
|
120
121
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
122
|
+
logging.warning("Image click handling failed", exc_info=True)
|
|
121
123
|
try:
|
|
122
124
|
event.ignore()
|
|
123
125
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
@@ -114,29 +114,11 @@ class DialogManager:
|
|
|
114
114
|
_template_dialog.activateWindow()
|
|
115
115
|
return
|
|
116
116
|
|
|
117
|
-
# Create new dialog
|
|
117
|
+
# Create new dialog. Picking a template arms it right away, and closing
|
|
118
|
+
# the dialog cleans the mode up, so there is nothing to do on finish.
|
|
118
119
|
self.host.template_dialog = UserTemplateDialog(self.host, self.host)
|
|
119
120
|
self.host.template_dialog.show()
|
|
120
121
|
|
|
121
|
-
# Activate if a template is selected after dialog is closed
|
|
122
|
-
def on_dialog_finished() -> None:
|
|
123
|
-
if self.host.template_dialog.selected_template:
|
|
124
|
-
template_name = self.host.template_dialog.selected_template.get(
|
|
125
|
-
"name", "user_template"
|
|
126
|
-
)
|
|
127
|
-
mode_name = f"template_user_{template_name}"
|
|
128
|
-
|
|
129
|
-
# Store template data for the scene to use
|
|
130
|
-
self.host.set_scene_user_template_data(
|
|
131
|
-
self.host.template_dialog.selected_template
|
|
132
|
-
)
|
|
133
|
-
self.host.ui_manager.set_mode(mode_name)
|
|
134
|
-
|
|
135
|
-
# Update status
|
|
136
|
-
self.host.update_status_message(f"Template mode: {template_name}")
|
|
137
|
-
|
|
138
|
-
self.host.template_dialog.finished.connect(on_dialog_finished)
|
|
139
|
-
|
|
140
122
|
def save_2d_as_template(self) -> None:
|
|
141
123
|
"""Save current 2D structure as a template"""
|
|
142
124
|
if not self.host.state_manager.data.atoms:
|
{moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/edit_actions_logic.py
RENAMED
|
@@ -883,9 +883,11 @@ class EditActionsManager:
|
|
|
883
883
|
self.host.set_current_file_path(None)
|
|
884
884
|
self.host.state_manager.update_window_title()
|
|
885
885
|
|
|
886
|
-
# Reset 2D zoom
|
|
886
|
+
# Reset 2D zoom to the launch default (75%), not the bare identity
|
|
887
|
+
# transform -- resetTransform() left the view at 100%, so Clear All
|
|
888
|
+
# came back zoomed in compared to a freshly started session.
|
|
887
889
|
if self.host.init_manager.view_2d:
|
|
888
|
-
self.host.
|
|
890
|
+
self.host.view_3d_manager.reset_zoom()
|
|
889
891
|
|
|
890
892
|
# Update scene and view
|
|
891
893
|
self.host.init_manager.scene.update()
|
{moleditpy_linux-4.7.2 → moleditpy_linux-4.8.1}/src/moleditpy_linux/ui/molecular_scene_handler.py
RENAMED
|
@@ -24,6 +24,7 @@ from .bond_item import BondItem
|
|
|
24
24
|
|
|
25
25
|
from ..utils.sip_isdeleted_safe import sip_isdeleted_safe
|
|
26
26
|
|
|
27
|
+
from ..core.mol_geometry import is_problematic_valence
|
|
27
28
|
from ..utils.constants import DEFAULT_BOND_LENGTH, SUM_TOLERANCE
|
|
28
29
|
|
|
29
30
|
|
|
@@ -197,6 +198,106 @@ class TemplateMixin:
|
|
|
197
198
|
|
|
198
199
|
return not atom1_has_other_double and not atom2_has_other_double
|
|
199
200
|
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _bond_load(atom: Optional[AtomItem]) -> float:
|
|
203
|
+
"""Total bond order already attached to *atom*."""
|
|
204
|
+
if atom is None:
|
|
205
|
+
return 0.0
|
|
206
|
+
return sum(b.order for b in atom.bonds)
|
|
207
|
+
|
|
208
|
+
def _plan_template_bonds(
|
|
209
|
+
self,
|
|
210
|
+
bonds: List[Tuple[int, int, int]],
|
|
211
|
+
vertex_atoms: List[Optional[AtomItem]],
|
|
212
|
+
) -> Tuple[List[Tuple[int, int, int]], List[Tuple[int, int]]]:
|
|
213
|
+
"""Decide the order every template bond ends up with once placed.
|
|
214
|
+
|
|
215
|
+
Two things constrain the template's own alternation:
|
|
216
|
+
|
|
217
|
+
* a bond that already exists is kept, unless the benzene policy allows
|
|
218
|
+
re-ordering it *and* both atoms have room for the extra order;
|
|
219
|
+
* a Kekule ring always carries a double next to its fused bond, so
|
|
220
|
+
fusing onto a single bond whose atoms each already hold a double --
|
|
221
|
+
every peripheral single bond of naphthalene -- would hand one of them
|
|
222
|
+
a fifth bond. Those bonds are lowered instead, which leaves the new
|
|
223
|
+
carbons as methylene: a real molecule rather than a hypervalent one.
|
|
224
|
+
|
|
225
|
+
Existing bonds are settled first so that an allowed upgrade is in the
|
|
226
|
+
atom's load before the new bonds hanging off it are fitted, and loads
|
|
227
|
+
are carried across the ring so several new bonds on one atom count
|
|
228
|
+
together.
|
|
229
|
+
|
|
230
|
+
Returns the planned orders and the vertex pairs the editor already
|
|
231
|
+
draws unchanged -- the ghost skips those, since painting a second copy
|
|
232
|
+
over a real double bond reads as a triple.
|
|
233
|
+
"""
|
|
234
|
+
is_benzene_template = len(bonds) == 6 and any(o == 2 for _, _, o in bonds)
|
|
235
|
+
loads = {i: self._bond_load(atom) for i, atom in enumerate(vertex_atoms)}
|
|
236
|
+
|
|
237
|
+
def vertex(index: int) -> Optional[AtomItem]:
|
|
238
|
+
return vertex_atoms[index] if index < len(vertex_atoms) else None
|
|
239
|
+
|
|
240
|
+
def fits(index: int, extra: float) -> bool:
|
|
241
|
+
atom = vertex(index)
|
|
242
|
+
if atom is None or extra <= 0:
|
|
243
|
+
return True
|
|
244
|
+
return not is_problematic_valence(
|
|
245
|
+
getattr(atom, "symbol", "C"),
|
|
246
|
+
loads.get(index, 0.0) + extra,
|
|
247
|
+
int(getattr(atom, "charge", 0) or 0),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
planned: Dict[int, int] = {}
|
|
251
|
+
unchanged: List[Tuple[int, int]] = []
|
|
252
|
+
|
|
253
|
+
# Every new bond needs at least a single, so an upgrade has to leave
|
|
254
|
+
# room for the ones still to come as well as for itself
|
|
255
|
+
incoming: Dict[int, int] = {}
|
|
256
|
+
for i, j, _order in bonds:
|
|
257
|
+
atom_i, atom_j = vertex(i), vertex(j)
|
|
258
|
+
if atom_i and atom_j and self.find_bond_between(atom_i, atom_j):
|
|
259
|
+
continue
|
|
260
|
+
incoming[i] = incoming.get(i, 0) + 1
|
|
261
|
+
incoming[j] = incoming.get(j, 0) + 1
|
|
262
|
+
|
|
263
|
+
# Pass 1 -- bonds that are already drawn
|
|
264
|
+
for position, (i, j, order) in enumerate(bonds):
|
|
265
|
+
atom_i, atom_j = vertex(i), vertex(j)
|
|
266
|
+
exist_b = (
|
|
267
|
+
self.find_bond_between(atom_i, atom_j) if atom_i and atom_j else None
|
|
268
|
+
)
|
|
269
|
+
if exist_b is None:
|
|
270
|
+
continue
|
|
271
|
+
current = int(getattr(exist_b, "order", order))
|
|
272
|
+
extra = order - current
|
|
273
|
+
if (
|
|
274
|
+
is_benzene_template
|
|
275
|
+
and self._should_overwrite_benzene_bond(exist_b)
|
|
276
|
+
and fits(i, extra + incoming.get(i, 0))
|
|
277
|
+
and fits(j, extra + incoming.get(j, 0))
|
|
278
|
+
):
|
|
279
|
+
loads[i] = loads.get(i, 0.0) + extra
|
|
280
|
+
loads[j] = loads.get(j, 0.0) + extra
|
|
281
|
+
planned[position] = order
|
|
282
|
+
else:
|
|
283
|
+
planned[position] = current
|
|
284
|
+
unchanged.append((i, j))
|
|
285
|
+
|
|
286
|
+
# Pass 2 -- bonds the placement will create
|
|
287
|
+
for position, (i, j, order) in enumerate(bonds):
|
|
288
|
+
if position in planned:
|
|
289
|
+
continue
|
|
290
|
+
while order > 1 and not (fits(i, order) and fits(j, order)):
|
|
291
|
+
order -= 1
|
|
292
|
+
loads[i] = loads.get(i, 0.0) + order
|
|
293
|
+
loads[j] = loads.get(j, 0.0) + order
|
|
294
|
+
planned[position] = order
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
[(i, j, planned[position]) for position, (i, j, _) in enumerate(bonds)],
|
|
298
|
+
unchanged,
|
|
299
|
+
)
|
|
300
|
+
|
|
200
301
|
def add_molecule_fragment(
|
|
201
302
|
self,
|
|
202
303
|
points: List[Union[QPointF, Tuple[float, float]]],
|
|
@@ -214,8 +315,6 @@ class TemplateMixin:
|
|
|
214
315
|
num_points = len(points)
|
|
215
316
|
atom_items: List[Optional[AtomItem]] = [None] * num_points
|
|
216
317
|
|
|
217
|
-
is_benzene_template = num_points == 6 and any(o == 2 for _, _, o in bonds_info)
|
|
218
|
-
|
|
219
318
|
def coords(p: Any) -> Any:
|
|
220
319
|
if hasattr(p, "x") and hasattr(p, "y"):
|
|
221
320
|
return (p.x(), p.y())
|
|
@@ -317,6 +416,12 @@ class TemplateMixin:
|
|
|
317
416
|
new_tb.append((i_idx, j_idx, new_order))
|
|
318
417
|
template_bonds_to_use = new_tb
|
|
319
418
|
|
|
419
|
+
# Atoms exist but carry no template bonds yet, so their loads are the
|
|
420
|
+
# ones the fusion has to fit into.
|
|
421
|
+
template_bonds_to_use, _unchanged = self._plan_template_bonds(
|
|
422
|
+
list(template_bonds_to_use), atom_items
|
|
423
|
+
)
|
|
424
|
+
|
|
320
425
|
# --- 5) Bond Creation/Update ---
|
|
321
426
|
for id1_idx, id2_idx, order in template_bonds_to_use:
|
|
322
427
|
if id1_idx < len(atom_items) and id2_idx < len(atom_items):
|
|
@@ -331,22 +436,15 @@ class TemplateMixin:
|
|
|
331
436
|
exist_b = self.find_bond_between(a_item, b_item)
|
|
332
437
|
|
|
333
438
|
if exist_b:
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
and self._should_overwrite_benzene_bond(exist_b)
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
if should_overwrite:
|
|
341
|
-
# Update bond order if conditions met
|
|
342
|
-
exist_b.order = order
|
|
343
|
-
exist_b.stereo = 0
|
|
344
|
-
self.data.bonds[(id1, id2)]["order"] = order
|
|
345
|
-
self.data.bonds[(id1, id2)]["stereo"] = 0
|
|
346
|
-
exist_b.update()
|
|
347
|
-
else:
|
|
348
|
-
# Keep existing bond if conditions not met
|
|
439
|
+
# _plan_template_bonds already applied the keep-or-overwrite
|
|
440
|
+
# policy, so an unchanged order means keep this bond
|
|
441
|
+
if order == exist_b.order:
|
|
349
442
|
continue
|
|
443
|
+
exist_b.order = order
|
|
444
|
+
exist_b.stereo = 0
|
|
445
|
+
self.data.bonds[(id1, id2)]["order"] = order
|
|
446
|
+
self.data.bonds[(id1, id2)]["stereo"] = 0
|
|
447
|
+
exist_b.update()
|
|
350
448
|
else:
|
|
351
449
|
# Create new bond
|
|
352
450
|
self.create_bond(a_item, b_item, bond_order=order, bond_stereo=0)
|
|
@@ -448,6 +546,7 @@ class TemplateMixin:
|
|
|
448
546
|
self.template_context["bonds_info"] = bonds_info
|
|
449
547
|
|
|
450
548
|
# Snap individual preview vertices to nearby atoms to reflect template fusing visually
|
|
549
|
+
vertex_atoms: List[Optional[AtomItem]] = [None] * len(points)
|
|
451
550
|
if self.get_setting("template_fusing_enabled_2d", True) and not alt_pressed:
|
|
452
551
|
fuse_dist = self.get_setting("template_fusing_distance_2d", 7.0)
|
|
453
552
|
mapped_atoms = set(self.template_context.get("items", []))
|
|
@@ -468,6 +567,7 @@ class TemplateMixin:
|
|
|
468
567
|
if best_idx != -1 and best_d <= click_map_threshold:
|
|
469
568
|
points[best_idx] = ex_pos
|
|
470
569
|
used_indices.add(best_idx)
|
|
570
|
+
vertex_atoms[best_idx] = ex_item
|
|
471
571
|
except (AttributeError, TypeError, IndexError):
|
|
472
572
|
# Safe defensive fallback catching AttributeError, TypeError, IndexError
|
|
473
573
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
@@ -497,8 +597,27 @@ class TemplateMixin:
|
|
|
497
597
|
if nearby and best_d <= fuse_dist:
|
|
498
598
|
points[i] = nearby.pos()
|
|
499
599
|
mapped_atoms.add(nearby)
|
|
600
|
+
vertex_atoms[i] = nearby
|
|
601
|
+
|
|
602
|
+
# add_molecule_fragment rotates a fused aromatic ring to match the
|
|
603
|
+
# bonds already there; the preview shows the same alternation. It
|
|
604
|
+
# rotates the placement copy itself, so template_context keeps the
|
|
605
|
+
# unrotated orders.
|
|
606
|
+
preview_bonds = bonds_info
|
|
607
|
+
if is_aromatic and n == 6:
|
|
608
|
+
rotation = self._calculate_6ring_rotation(n, bonds_info, vertex_atoms)
|
|
609
|
+
orders = [order for (_, _, order) in bonds_info]
|
|
610
|
+
preview_bonds = [
|
|
611
|
+
(i, j, orders[(m + rotation) % n])
|
|
612
|
+
for m, (i, j, _) in enumerate(bonds_info)
|
|
613
|
+
]
|
|
500
614
|
|
|
501
|
-
self.
|
|
615
|
+
preview_bonds, editor_drawn = self._plan_template_bonds(
|
|
616
|
+
preview_bonds, vertex_atoms
|
|
617
|
+
)
|
|
618
|
+
self.template_preview.set_geometry(
|
|
619
|
+
points, is_aromatic, preview_bonds, editor_drawn
|
|
620
|
+
)
|
|
502
621
|
|
|
503
622
|
self.template_preview.show()
|
|
504
623
|
if self.views():
|
|
@@ -555,6 +674,44 @@ class TemplateMixin:
|
|
|
555
674
|
points.append(current_p)
|
|
556
675
|
return points
|
|
557
676
|
|
|
677
|
+
def _apply_template_atom_to_existing(
|
|
678
|
+
self, atom_item: Any, atom_data: Dict[str, Any]
|
|
679
|
+
) -> None:
|
|
680
|
+
"""Overwrite an existing atom with a user template atom's element/charge/radical."""
|
|
681
|
+
symbol = atom_data.get("symbol", "C")
|
|
682
|
+
# A hand-written template may carry nulls here
|
|
683
|
+
charge = int(atom_data.get("charge") or 0)
|
|
684
|
+
radical = int(atom_data.get("radical") or 0)
|
|
685
|
+
|
|
686
|
+
if (
|
|
687
|
+
atom_item.symbol == symbol
|
|
688
|
+
and atom_item.charge == charge
|
|
689
|
+
and atom_item.radical == radical
|
|
690
|
+
):
|
|
691
|
+
return
|
|
692
|
+
|
|
693
|
+
try:
|
|
694
|
+
atom_item.prepareGeometryChange()
|
|
695
|
+
atom_item.symbol = symbol
|
|
696
|
+
atom_item.charge = charge
|
|
697
|
+
atom_item.radical = radical
|
|
698
|
+
|
|
699
|
+
record = self.data.atoms.get(atom_item.atom_id)
|
|
700
|
+
if record is not None:
|
|
701
|
+
record["symbol"] = symbol
|
|
702
|
+
record["charge"] = charge
|
|
703
|
+
record["radical"] = radical
|
|
704
|
+
|
|
705
|
+
# A bonded carbon is drawn as a bare vertex; the new element needs a label
|
|
706
|
+
atom_item.update_style()
|
|
707
|
+
for bond in atom_item.bonds:
|
|
708
|
+
bond.update()
|
|
709
|
+
other = bond.atom1 if bond.atom2 is atom_item else bond.atom2
|
|
710
|
+
if other is not None:
|
|
711
|
+
other.update_style()
|
|
712
|
+
except (AttributeError, RuntimeError, KeyError, TypeError) as e:
|
|
713
|
+
logging.warning(f"Error applying template atom to existing atom: {e}")
|
|
714
|
+
|
|
558
715
|
def add_user_template_fragment(self, context: Dict[str, Any]) -> None:
|
|
559
716
|
"""Place user template fragment"""
|
|
560
717
|
points = context.get("points", [])
|
|
@@ -569,9 +726,10 @@ class TemplateMixin:
|
|
|
569
726
|
atom_id_map = {} # template id -> scene atom id
|
|
570
727
|
|
|
571
728
|
for i, (pos, atom_data) in enumerate(zip(points, atoms_data)):
|
|
572
|
-
#
|
|
729
|
+
# Reuse the clicked atom for the template's first atom, overwriting its element
|
|
573
730
|
if i == 0 and attachment_atom:
|
|
574
731
|
atom_id_map[atom_data["id"]] = attachment_atom.atom_id
|
|
732
|
+
self._apply_template_atom_to_existing(attachment_atom, atom_data)
|
|
575
733
|
continue
|
|
576
734
|
|
|
577
735
|
symbol = atom_data.get("symbol", "C")
|