MoleditPy-linux 2.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. moleditpy_linux/__init__.py +17 -0
  2. moleditpy_linux/__main__.py +29 -0
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +41 -0
  5. moleditpy_linux/modules/about_dialog.py +104 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +292 -0
  7. moleditpy_linux/modules/alignment_dialog.py +272 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/file_icon.ico +0 -0
  11. moleditpy_linux/modules/assets/icon.icns +0 -0
  12. moleditpy_linux/modules/assets/icon.ico +0 -0
  13. moleditpy_linux/modules/assets/icon.png +0 -0
  14. moleditpy_linux/modules/atom_item.py +395 -0
  15. moleditpy_linux/modules/bond_item.py +464 -0
  16. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  17. moleditpy_linux/modules/calculation_worker.py +766 -0
  18. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  19. moleditpy_linux/modules/constants.py +88 -0
  20. moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
  21. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  22. moleditpy_linux/modules/custom_qt_interactor.py +102 -0
  23. moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
  24. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  25. moleditpy_linux/modules/main_window.py +850 -0
  26. moleditpy_linux/modules/main_window_app_state.py +787 -0
  27. moleditpy_linux/modules/main_window_compute.py +1242 -0
  28. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  29. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  30. moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
  31. moleditpy_linux/modules/main_window_export.py +917 -0
  32. moleditpy_linux/modules/main_window_main_init.py +2100 -0
  33. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  34. moleditpy_linux/modules/main_window_project_io.py +434 -0
  35. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  36. moleditpy_linux/modules/main_window_ui_manager.py +602 -0
  37. moleditpy_linux/modules/main_window_view_3d.py +1539 -0
  38. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  39. moleditpy_linux/modules/mirror_dialog.py +122 -0
  40. moleditpy_linux/modules/molecular_data.py +302 -0
  41. moleditpy_linux/modules/molecule_scene.py +2000 -0
  42. moleditpy_linux/modules/move_group_dialog.py +600 -0
  43. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  44. moleditpy_linux/modules/planarize_dialog.py +220 -0
  45. moleditpy_linux/modules/plugin_interface.py +215 -0
  46. moleditpy_linux/modules/plugin_manager.py +473 -0
  47. moleditpy_linux/modules/plugin_manager_window.py +274 -0
  48. moleditpy_linux/modules/settings_dialog.py +1503 -0
  49. moleditpy_linux/modules/template_preview_item.py +157 -0
  50. moleditpy_linux/modules/template_preview_view.py +74 -0
  51. moleditpy_linux/modules/translation_dialog.py +364 -0
  52. moleditpy_linux/modules/user_template_dialog.py +692 -0
  53. moleditpy_linux/modules/zoomable_view.py +129 -0
  54. moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
  55. moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
  56. moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
  57. moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
  58. moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
  59. moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,321 @@
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 PyQt6.QtWidgets import (
14
+ QDialog, QVBoxLayout, QGridLayout, QPushButton, QHBoxLayout, QLabel,
15
+ QApplication, QColorDialog
16
+ )
17
+ from PyQt6.QtGui import QColor
18
+ try:
19
+ from .constants import CPK_COLORS, DEFAULT_CPK_COLORS
20
+ except Exception:
21
+ from modules.constants import CPK_COLORS, DEFAULT_CPK_COLORS
22
+
23
+ class ColorSettingsDialog(QDialog):
24
+ """Dialog to customize CPK element colors.
25
+
26
+ - Click an element to pick a new color for the element (CPK colors).
27
+ - Reset All button to restore defaults for everything.
28
+ """
29
+ def __init__(self, current_settings, parent=None):
30
+ super().__init__(parent)
31
+ self.setWindowTitle("CPK Colors")
32
+ self.parent_window = parent
33
+ self.current_settings = current_settings or {}
34
+
35
+ self.changed_cpk = {} # symbol -> hex
36
+ self._reset_all_flag = False
37
+
38
+ layout = QVBoxLayout(self)
39
+
40
+ # Color picking for CPK is available in the periodic table and CPK dialog
41
+
42
+ # Periodic table grid (buttons like PeriodicTableDialog)
43
+ grid = QGridLayout()
44
+ self.element_buttons = {}
45
+ elements = [
46
+ ('H',1,1), ('He',1,18),
47
+ ('Li',2,1), ('Be',2,2), ('B',2,13), ('C',2,14), ('N',2,15), ('O',2,16), ('F',2,17), ('Ne',2,18),
48
+ ('Na',3,1), ('Mg',3,2), ('Al',3,13), ('Si',3,14), ('P',3,15), ('S',3,16), ('Cl',3,17), ('Ar',3,18),
49
+ ('K',4,1), ('Ca',4,2), ('Sc',4,3), ('Ti',4,4), ('V',4,5), ('Cr',4,6), ('Mn',4,7), ('Fe',4,8),
50
+ ('Co',4,9), ('Ni',4,10), ('Cu',4,11), ('Zn',4,12), ('Ga',4,13), ('Ge',4,14), ('As',4,15), ('Se',4,16),
51
+ ('Br',4,17), ('Kr',4,18),
52
+ ('Rb',5,1), ('Sr',5,2), ('Y',5,3), ('Zr',5,4), ('Nb',5,5), ('Mo',5,6), ('Tc',5,7), ('Ru',5,8),
53
+ ('Rh',5,9), ('Pd',5,10), ('Ag',5,11), ('Cd',5,12), ('In',5,13), ('Sn',5,14), ('Sb',5,15), ('Te',5,16),
54
+ ('I',5,17), ('Xe',5,18),
55
+ ('Cs',6,1), ('Ba',6,2), ('Hf',6,4), ('Ta',6,5), ('W',6,6), ('Re',6,7), ('Os',6,8),
56
+ ('Ir',6,9), ('Pt',6,10), ('Au',6,11), ('Hg',6,12), ('Tl',6,13), ('Pb',6,14), ('Bi',6,15), ('Po',6,16),
57
+ ('At',6,17), ('Rn',6,18),
58
+ ('Fr',7,1), ('Ra',7,2), ('Rf',7,4), ('Db',7,5), ('Sg',7,6), ('Bh',7,7), ('Hs',7,8),
59
+ ('Mt',7,9), ('Ds',7,10), ('Rg',7,11), ('Cn',7,12), ('Nh',7,13), ('Fl',7,14), ('Mc',7,15), ('Lv',7,16),
60
+ ('Ts',7,17), ('Og',7,18),
61
+ ('La',8,3), ('Ce',8,4), ('Pr',8,5), ('Nd',8,6), ('Pm',8,7), ('Sm',8,8), ('Eu',8,9), ('Gd',8,10), ('Tb',8,11),
62
+ ('Dy',8,12), ('Ho',8,13), ('Er',8,14), ('Tm',8,15), ('Yb',8,16), ('Lu',8,17),
63
+ ('Ac',9,3), ('Th',9,4), ('Pa',9,5), ('U',9,6), ('Np',9,7), ('Pu',9,8), ('Am',9,9), ('Cm',9,10), ('Bk',9,11),
64
+ ('Cf',9,12), ('Es',9,13), ('Fm',9,14), ('Md',9,15), ('No',9,16), ('Lr',9,17),
65
+ ]
66
+
67
+ for symbol, row, col in elements:
68
+ b = QPushButton(symbol)
69
+ b.setFixedSize(40, 40)
70
+ # Choose override color (if present) else default CPK color
71
+ override = self.current_settings.get('cpk_colors', {}).get(symbol)
72
+ if override:
73
+ q_color = QColor(override)
74
+ else:
75
+ q_color = CPK_COLORS.get(symbol, CPK_COLORS['DEFAULT'])
76
+
77
+ brightness = (q_color.red() * 299 + q_color.green() * 587 + q_color.blue() * 114) / 1000
78
+ text_color = 'white' if brightness < 128 else 'black'
79
+ b.setStyleSheet(f"background-color: {q_color.name()}; color: {text_color}; border: 1px solid #555; font-weight: bold;")
80
+ b.clicked.connect(self.on_element_clicked)
81
+ grid.addWidget(b, row, col)
82
+ self.element_buttons[symbol] = b
83
+
84
+ layout.addLayout(grid)
85
+
86
+ # Ball & Stick bond color (3D) picker - placed near the periodic table for CPK settings
87
+ self.changed_bs_color = None
88
+ try:
89
+ bs_h = QHBoxLayout()
90
+ bs_label = QLabel("Ball & Stick bond color:")
91
+ self.bs_button = QPushButton()
92
+ self.bs_button.setFixedSize(36, 24)
93
+ # initialize from current settings (if provided)
94
+ try:
95
+ cur_bs = self.current_settings.get('ball_stick_bond_color') if self.current_settings else None
96
+ except Exception:
97
+ cur_bs = None
98
+ if not cur_bs and self.parent_window and hasattr(self.parent_window, 'settings'):
99
+ cur_bs = self.parent_window.settings.get('ball_stick_bond_color', '#7F7F7F')
100
+ try:
101
+ self.bs_button.setStyleSheet(f"background-color: {cur_bs}; border: 1px solid #888;")
102
+ self.bs_button.setToolTip(cur_bs)
103
+ except Exception:
104
+ pass
105
+ self.bs_button.clicked.connect(self.pick_bs_bond_color)
106
+ bs_h.addWidget(bs_label)
107
+ bs_h.addWidget(self.bs_button)
108
+ bs_h.addStretch(1)
109
+ layout.addLayout(bs_h)
110
+ except Exception:
111
+ pass
112
+
113
+ # Reset button and action buttons
114
+ h = QHBoxLayout()
115
+ reset_button = QPushButton("Reset All")
116
+ reset_button.clicked.connect(self.reset_all)
117
+ h.addWidget(reset_button)
118
+ h.addStretch(1)
119
+ apply_button = QPushButton("Apply")
120
+ apply_button.clicked.connect(self.apply_changes)
121
+ ok_button = QPushButton("OK")
122
+ ok_button.clicked.connect(self.accept)
123
+ cancel_button = QPushButton("Cancel")
124
+ cancel_button.clicked.connect(self.reject)
125
+
126
+ h.addWidget(apply_button); h.addWidget(ok_button); h.addWidget(cancel_button)
127
+ layout.addLayout(h)
128
+
129
+ # initialize
130
+ # No 2D bond color control here
131
+
132
+ # 2D bond color picker removed — 2D bond color is fixed and not configurable here
133
+
134
+ def on_element_clicked(self):
135
+ b = self.sender()
136
+ symbol = b.text()
137
+ # get current color (override if exists else default)
138
+ cur = self.current_settings.get('cpk_colors', {}).get(symbol)
139
+ if not cur:
140
+ cur = CPK_COLORS.get(symbol, CPK_COLORS['DEFAULT']).name()
141
+ color = QColorDialog.getColor(QColor(cur), self)
142
+ if color.isValid():
143
+ self.changed_cpk[symbol] = color.name()
144
+ # Update button appearance
145
+ brightness = (color.red() * 299 + color.green() * 587 + color.blue() * 114) / 1000
146
+ text_color = 'white' if brightness < 128 else 'black'
147
+ b.setStyleSheet(f"background-color: {color.name()}; color: {text_color}; border: 1px solid #555; font-weight: bold;")
148
+
149
+ def reset_all(self):
150
+ # Clear overrides
151
+ self.changed_cpk = {}
152
+ self._reset_all_flag = True
153
+
154
+ # 1. B&S結合色もリセット対象(デフォルト値)に設定
155
+ try:
156
+ self.changed_bs_color = self.parent_window.default_settings.get('ball_stick_bond_color', '#7F7F7F') if hasattr(self.parent_window, 'default_settings') else '#7F7F7F'
157
+ except Exception:
158
+ self.changed_bs_color = '#7F7F7F'
159
+
160
+ # 2. ダイアログ内のCPKボタンの表示をデフォルトに戻す
161
+ for s, btn in self.element_buttons.items():
162
+ q_color = DEFAULT_CPK_COLORS.get(s, DEFAULT_CPK_COLORS['DEFAULT'])
163
+ brightness = (q_color.red() * 299 + q_color.green() * 587 + q_color.blue() * 114) / 1000
164
+ text_color = 'white' if brightness < 128 else 'black'
165
+ btn.setStyleSheet(f"background-color: {q_color.name()}; color: {text_color}; border: 1px solid #555; font-weight: bold;")
166
+
167
+ # 3. 3Dプレビューを更新する L.3337〜L.3386 の try...finally ブロックは削除
168
+
169
+ # 4. ダイアログ内のB&S結合色ボタンの表示をデフォルトに戻す
170
+ try:
171
+ if hasattr(self, 'bs_button'):
172
+ # self.changed_bs_color に設定したデフォルト値を反映
173
+ hexv = self.changed_bs_color
174
+ self.bs_button.setStyleSheet(f"background-color: {hexv}; border: 1px solid #888;")
175
+ self.bs_button.setToolTip(hexv)
176
+ except Exception:
177
+ pass
178
+
179
+ def apply_changes(self):
180
+ # Persist only changed keys
181
+ if self.parent_window:
182
+ if self._reset_all_flag:
183
+ # Remove any cpk overrides
184
+ try:
185
+ if 'cpk_colors' in self.parent_window.settings:
186
+ del self.parent_window.settings['cpk_colors']
187
+ except Exception:
188
+ pass
189
+ if self.changed_cpk:
190
+ # Merge with existing overrides
191
+ cdict = self.parent_window.settings.get('cpk_colors', {}).copy()
192
+ cdict.update(self.changed_cpk)
193
+ self.parent_window.settings['cpk_colors'] = cdict
194
+ self.parent_window.settings_dirty = True
195
+ # After changing settings, update global CPK color map and refresh views
196
+ try:
197
+ self.parent_window.update_cpk_colors_from_settings()
198
+ except Exception:
199
+ pass
200
+ try:
201
+ self.parent_window.apply_3d_settings(redraw=False)
202
+ except Exception:
203
+ pass
204
+ try:
205
+ if hasattr(self.parent_window, 'current_mol') and self.parent_window.current_mol:
206
+ self.parent_window.draw_molecule_3d(self.parent_window.current_mol)
207
+ except Exception:
208
+ pass
209
+ # update 2D scene objects
210
+ try:
211
+ if hasattr(self.parent_window, 'scene'):
212
+ for it in self.parent_window.scene.items():
213
+ try:
214
+ if hasattr(it, 'update_style'):
215
+ it.update_style()
216
+ except Exception:
217
+ pass
218
+ except Exception:
219
+ pass
220
+ # update periodic table button styles in the dialog to reflect any overrides
221
+ try:
222
+ for s, btn in self.element_buttons.items():
223
+ try:
224
+ q_color = QColor(self.parent_window.settings.get('cpk_colors', {}).get(s, CPK_COLORS.get(s, CPK_COLORS['DEFAULT']).name()))
225
+ brightness = (q_color.red() * 299 + q_color.green() * 587 + q_color.blue() * 114) / 1000
226
+ text_color = 'white' if brightness < 128 else 'black'
227
+ btn.setStyleSheet(f"background-color: {q_color.name()}; color: {text_color}; border: 1px solid #555; font-weight: bold;")
228
+ except Exception:
229
+ pass
230
+ except Exception:
231
+ pass
232
+ # Refresh any open SettingsDialog instances so the ball & stick color preview updates
233
+ try:
234
+ # Avoid circular import at module level; import SettingsDialog on demand
235
+ try:
236
+ from .settings_dialog import SettingsDialog
237
+ except Exception:
238
+ from modules.settings_dialog import SettingsDialog
239
+
240
+ for w in QApplication.topLevelWidgets():
241
+ try:
242
+ if isinstance(w, SettingsDialog):
243
+ try:
244
+ w.update_ui_from_settings(self.parent_window.settings)
245
+ except Exception:
246
+ pass
247
+ except Exception:
248
+ pass
249
+ except Exception:
250
+ pass
251
+ # Persist changed Ball & Stick color if the user changed it from the CPK dialog
252
+ if getattr(self, 'changed_bs_color', None):
253
+ try:
254
+ self.parent_window.settings['ball_stick_bond_color'] = self.changed_bs_color
255
+ try:
256
+ self.parent_window.settings_dirty = True
257
+ except Exception:
258
+ pass
259
+ # After changing ball-stick color, ensure 3D view updates
260
+ try:
261
+ self.parent_window.apply_3d_settings()
262
+ except Exception:
263
+ pass
264
+ try:
265
+ if hasattr(self.parent_window, 'current_mol') and self.parent_window.current_mol:
266
+ self.parent_window.draw_molecule_3d(self.parent_window.current_mol)
267
+ except Exception:
268
+ pass
269
+ except Exception:
270
+ pass
271
+ # Removed 2D bond color control — nothing to persist here
272
+ elif self._reset_all_flag:
273
+ # Reset Ball & Stick 3D bond color to default
274
+ try:
275
+ # Use a stable default instead of relying on parent_window.default_settings
276
+ self.parent_window.settings['ball_stick_bond_color'] = '#7F7F7F'
277
+ try:
278
+ self.parent_window.settings_dirty = True
279
+ except Exception:
280
+ pass
281
+ except Exception:
282
+ pass
283
+ self.parent_window.update_cpk_colors_from_settings()
284
+ self.parent_window.apply_3d_settings()
285
+ if hasattr(self.parent_window, 'current_mol') and self.parent_window.current_mol:
286
+ self.parent_window.draw_molecule_3d(self.parent_window.current_mol)
287
+
288
+ # update 2D scene
289
+ try:
290
+ if hasattr(self.parent_window, 'scene'):
291
+ for it in self.parent_window.scene.items():
292
+ try:
293
+ # AtomItem.update_style uses CPK_COLORS map
294
+ if hasattr(it, 'update_style'):
295
+ it.update_style()
296
+ except Exception:
297
+ pass
298
+ except Exception:
299
+ pass
300
+
301
+ def accept(self):
302
+ self.apply_changes()
303
+ super().accept()
304
+
305
+ def pick_bs_bond_color(self):
306
+ """Pick Ball & Stick 3D bond color from the CPK dialog and update preview immediately."""
307
+ try:
308
+ cur = getattr(self, 'changed_bs_color', None) or (self.current_settings.get('ball_stick_bond_color') if self.current_settings else None)
309
+ except Exception:
310
+ cur = None
311
+ if not cur and self.parent_window and hasattr(self.parent_window, 'settings'):
312
+ cur = self.parent_window.settings.get('ball_stick_bond_color', '#7F7F7F')
313
+ color = QColorDialog.getColor(QColor(cur), self)
314
+ if color.isValid():
315
+ hexv = color.name()
316
+ self.changed_bs_color = hexv
317
+ try:
318
+ self.bs_button.setStyleSheet(f"background-color: {hexv}; border: 1px solid #888;")
319
+ self.bs_button.setToolTip(hexv)
320
+ except Exception:
321
+ pass
@@ -0,0 +1,88 @@
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
+ # --- Constants ---
14
+
15
+ from PyQt6.QtGui import QFont, QColor
16
+ from rdkit import Chem
17
+
18
+ #Version
19
+ VERSION = '2.4.1'
20
+
21
+ ATOM_RADIUS = 18
22
+ BOND_OFFSET = 3.5
23
+ DEFAULT_BOND_LENGTH = 75 # テンプレートで使用する標準結合長
24
+ CLIPBOARD_MIME_TYPE = "application/x-moleditpy-fragment"
25
+
26
+ # Physical bond length (approximate) used to convert scene pixels to angstroms.
27
+ # DEFAULT_BOND_LENGTH is the length in pixels used in the editor UI for a typical bond.
28
+ # Many molecular file formats expect coordinates in angstroms; use ~1.5 Å as a typical single-bond length.
29
+ DEFAULT_BOND_LENGTH_ANGSTROM = 1.5
30
+ # Multiply pixel coordinates by this to get angstroms: ANGSTROM_PER_PIXEL = 1.5Å / DEFAULT_BOND_LENGTH(px)
31
+ ANGSTROM_PER_PIXEL = DEFAULT_BOND_LENGTH_ANGSTROM / DEFAULT_BOND_LENGTH
32
+
33
+ # UI / drawing / behavior constants (centralized for maintainability)
34
+ FONT_FAMILY = "Arial"
35
+ FONT_SIZE_LARGE = 20
36
+ FONT_SIZE_SMALL = 12
37
+ FONT_WEIGHT_BOLD = QFont.Weight.Bold
38
+
39
+ # Hit / visual sizes (in pixels at scale=1)
40
+ DESIRED_ATOM_PIXEL_RADIUS = 15.0
41
+ DESIRED_BOND_PIXEL_WIDTH = 18.0
42
+
43
+ # Bond/EZ label
44
+ EZ_LABEL_TEXT_OUTLINE = 2.5
45
+ EZ_LABEL_MARGIN = 16
46
+ EZ_LABEL_BOX_SIZE = 28
47
+
48
+ # Interaction thresholds
49
+ SNAP_DISTANCE = 14.0
50
+ SUM_TOLERANCE = 5.0
51
+
52
+ # Misc drawing
53
+ NUM_DASHES = 8
54
+ HOVER_PEN_WIDTH = 8
55
+
56
+ CPK_COLORS = {
57
+ 'H': QColor('#FFFFFF'), 'C': QColor('#222222'), 'N': QColor('#3377FF'), 'O': QColor('#FF3333'), 'F': QColor('#99E6E6'),
58
+ 'Cl': QColor('#33FF33'), 'Br': QColor('#A52A2A'), 'I': QColor('#9400D3'), 'S': QColor('#FFC000'), 'P': QColor('#FF8000'),
59
+ 'Si': QColor('#DAA520'), 'B': QColor('#FA8072'), 'He': QColor('#D9FFFF'), 'Ne': QColor('#B3E3F5'), 'Ar': QColor('#80D1E3'),
60
+ 'Kr': QColor('#5CACC8'), 'Xe': QColor('#429EB0'), 'Rn': QColor('#298FA2'), 'Li': QColor('#CC80FF'), 'Na': QColor('#AB5CF2'),
61
+ 'K': QColor('#8F44D7'), 'Rb': QColor('#702EBC'), 'Cs': QColor('#561B9E'), 'Fr': QColor('#421384'), 'Be': QColor('#C2FF00'),
62
+ 'Mg': QColor('#8AFF00'), 'Ca': QColor('#3DFF00'), 'Sr': QColor('#00FF00'), 'Ba': QColor('#00E600'), 'Ra': QColor('#00B800'),
63
+ 'Sc': QColor('#E6E6E6'), 'Ti': QColor('#BFC2C7'), 'V': QColor('#A6A6AB'), 'Cr': QColor('#8A99C7'), 'Mn': QColor('#9C7AC7'),
64
+ 'Fe': QColor('#E06633'), 'Co': QColor('#F090A0'), 'Ni': QColor('#50D050'), 'Cu': QColor('#C88033'), 'Zn': QColor('#7D80B0'),
65
+ 'Ga': QColor('#C28F8F'), 'Ge': QColor('#668F8F'), 'As': QColor('#BD80E3'), 'Se': QColor('#FFA100'), 'Tc': QColor('#3B9E9E'),
66
+ 'Ru': QColor('#248F8F'), 'Rh': QColor('#0A7D8F'), 'Pd': QColor('#006985'), 'Ag': QColor('#C0C0C0'), 'Cd': QColor('#FFD700'),
67
+ 'In': QColor('#A67573'), 'Sn': QColor('#668080'), 'Sb': QColor('#9E63B5'), 'Te': QColor('#D47A00'), 'La': QColor('#70D4FF'),
68
+ 'Ce': QColor('#FFFFC7'), 'Pr': QColor('#D9FFC7'), 'Nd': QColor('#C7FFC7'), 'Pm': QColor('#A3FFC7'), 'Sm': QColor('#8FFFC7'),
69
+ 'Eu': QColor('#61FFC7'), 'Gd': QColor('#45FFC7'), 'Tb': QColor('#30FFC7'), 'Dy': QColor('#1FFFC7'), 'Ho': QColor('#00FF9C'),
70
+ 'Er': QColor('#00E675'), 'Tm': QColor('#00D452'), 'Yb': QColor('#00BF38'), 'Lu': QColor('#00AB24'), 'Hf': QColor('#4DC2FF'),
71
+ 'Ta': QColor('#4DA6FF'), 'W': QColor('#2194D6'), 'Re': QColor('#267DAB'), 'Os': QColor('#266696'), 'Ir': QColor('#175487'),
72
+ 'Pt': QColor('#D0D0E0'), 'Au': QColor('#FFD123'), 'Hg': QColor('#B8B8D0'), 'Tl': QColor('#A6544D'), 'Pb': QColor('#575961'),
73
+ 'Bi': QColor('#9E4FB5'), 'Po': QColor('#AB5C00'), 'At': QColor('#754F45'), 'Ac': QColor('#70ABFA'), 'Th': QColor('#00BAFF'),
74
+ 'Pa': QColor('#00A1FF'), 'U': QColor('#008FFF'), 'Np': QColor('#0080FF'), 'Pu': QColor('#006BFF'), 'Am': QColor('#545CF2'),
75
+ 'Cm': QColor('#785CE3'), 'Bk': QColor('#8A4FE3'), 'Cf': QColor('#A136D4'), 'Es': QColor('#B31FD4'), 'Fm': QColor('#B31FBA'),
76
+ 'Md': QColor('#B30DA6'), 'No': QColor('#BD0D87'), 'Lr': QColor('#C70066'), 'Al': QColor('#B3A68F'), 'Y': QColor('#99FFFF'),
77
+ 'Zr': QColor('#7EE7E7'), 'Nb': QColor('#68CFCE'), 'Mo': QColor('#52B7B7'), 'DEFAULT': QColor('#FF1493') # Pink fallback
78
+ }
79
+ CPK_COLORS_PV = {
80
+ k: [c.redF(), c.greenF(), c.blueF()] for k, c in CPK_COLORS.items()
81
+ }
82
+
83
+ # Keep a copy of the original default map so we can restore it when user resets
84
+ DEFAULT_CPK_COLORS = {k: QColor(v) if not isinstance(v, QColor) else v for k, v in CPK_COLORS.items()}
85
+
86
+ pt = Chem.GetPeriodicTable()
87
+ VDW_RADII = {pt.GetElementSymbol(i): pt.GetRvdw(i) * 0.3 for i in range(1, 119)}
88
+