MoleditPy 1.16.3__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 (54) hide show
  1. moleditpy/__init__.py +4 -0
  2. moleditpy/__main__.py +29 -0
  3. moleditpy/main.py +37 -0
  4. moleditpy/modules/__init__.py +36 -0
  5. moleditpy/modules/about_dialog.py +92 -0
  6. moleditpy/modules/align_plane_dialog.py +281 -0
  7. moleditpy/modules/alignment_dialog.py +261 -0
  8. moleditpy/modules/analysis_window.py +197 -0
  9. moleditpy/modules/angle_dialog.py +428 -0
  10. moleditpy/modules/assets/icon.icns +0 -0
  11. moleditpy/modules/assets/icon.ico +0 -0
  12. moleditpy/modules/assets/icon.png +0 -0
  13. moleditpy/modules/atom_item.py +336 -0
  14. moleditpy/modules/bond_item.py +303 -0
  15. moleditpy/modules/bond_length_dialog.py +368 -0
  16. moleditpy/modules/calculation_worker.py +754 -0
  17. moleditpy/modules/color_settings_dialog.py +309 -0
  18. moleditpy/modules/constants.py +76 -0
  19. moleditpy/modules/constrained_optimization_dialog.py +667 -0
  20. moleditpy/modules/custom_interactor_style.py +737 -0
  21. moleditpy/modules/custom_qt_interactor.py +49 -0
  22. moleditpy/modules/dialog3_d_picking_mixin.py +96 -0
  23. moleditpy/modules/dihedral_dialog.py +431 -0
  24. moleditpy/modules/main_window.py +830 -0
  25. moleditpy/modules/main_window_app_state.py +747 -0
  26. moleditpy/modules/main_window_compute.py +1203 -0
  27. moleditpy/modules/main_window_dialog_manager.py +454 -0
  28. moleditpy/modules/main_window_edit_3d.py +531 -0
  29. moleditpy/modules/main_window_edit_actions.py +1449 -0
  30. moleditpy/modules/main_window_export.py +744 -0
  31. moleditpy/modules/main_window_main_init.py +1668 -0
  32. moleditpy/modules/main_window_molecular_parsers.py +1037 -0
  33. moleditpy/modules/main_window_project_io.py +429 -0
  34. moleditpy/modules/main_window_string_importers.py +270 -0
  35. moleditpy/modules/main_window_ui_manager.py +567 -0
  36. moleditpy/modules/main_window_view_3d.py +1211 -0
  37. moleditpy/modules/main_window_view_loaders.py +350 -0
  38. moleditpy/modules/mirror_dialog.py +110 -0
  39. moleditpy/modules/molecular_data.py +290 -0
  40. moleditpy/modules/molecule_scene.py +1964 -0
  41. moleditpy/modules/move_group_dialog.py +586 -0
  42. moleditpy/modules/periodic_table_dialog.py +72 -0
  43. moleditpy/modules/planarize_dialog.py +209 -0
  44. moleditpy/modules/settings_dialog.py +1071 -0
  45. moleditpy/modules/template_preview_item.py +148 -0
  46. moleditpy/modules/template_preview_view.py +62 -0
  47. moleditpy/modules/translation_dialog.py +353 -0
  48. moleditpy/modules/user_template_dialog.py +621 -0
  49. moleditpy/modules/zoomable_view.py +98 -0
  50. moleditpy-1.16.3.dist-info/METADATA +274 -0
  51. moleditpy-1.16.3.dist-info/RECORD +54 -0
  52. moleditpy-1.16.3.dist-info/WHEEL +5 -0
  53. moleditpy-1.16.3.dist-info/entry_points.txt +2 -0
  54. moleditpy-1.16.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,429 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ main_window_project_io.py
6
+ MainWindow (main_window.py) から分離されたモジュール
7
+ 機能クラス: MainWindowProjectIo
8
+ """
9
+
10
+
11
+ import pickle
12
+ import os
13
+ import json
14
+ import traceback
15
+
16
+
17
+ # RDKit imports (explicit to satisfy flake8 and used features)
18
+ try:
19
+ pass
20
+ except Exception:
21
+ pass
22
+
23
+ # PyQt6 Modules
24
+ from PyQt6.QtWidgets import (
25
+ QFileDialog, QMessageBox
26
+ )
27
+
28
+
29
+
30
+ from PyQt6.QtCore import (
31
+ QTimer
32
+ )
33
+
34
+
35
+ # Use centralized Open Babel availability from package-level __init__
36
+ # Use per-package modules availability (local __init__).
37
+ try:
38
+ from . import OBABEL_AVAILABLE
39
+ except Exception:
40
+ from modules import OBABEL_AVAILABLE
41
+ # Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
42
+ if OBABEL_AVAILABLE:
43
+ try:
44
+ from openbabel import pybel
45
+ except Exception:
46
+ # If import fails here, disable OBABEL locally; avoid raising
47
+ pybel = None
48
+ OBABEL_AVAILABLE = False
49
+ print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
50
+ else:
51
+ pybel = None
52
+
53
+ # Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
54
+ # allows safely detecting C++ wrapper objects that have been deleted. Import
55
+ # it once at module import time and expose a small, robust wrapper so callers
56
+ # can avoid re-importing sip repeatedly and so we centralize exception
57
+ # handling (this reduces crash risk during teardown and deletion operations).
58
+ try:
59
+ import sip as _sip # type: ignore
60
+ _sip_isdeleted = getattr(_sip, 'isdeleted', None)
61
+ except Exception:
62
+ _sip = None
63
+ _sip_isdeleted = None
64
+
65
+ try:
66
+ # package relative imports (preferred when running as `python -m moleditpy`)
67
+ pass
68
+ except Exception:
69
+ # Fallback to absolute imports for script-style execution
70
+ pass
71
+
72
+
73
+ # --- クラス定義 ---
74
+ class MainWindowProjectIo(object):
75
+ """ main_window.py から分離された機能クラス """
76
+
77
+ def __init__(self, main_window):
78
+ """ クラスの初期化 """
79
+ self.mw = main_window
80
+
81
+
82
+ def save_project(self):
83
+ """上書き保存(Ctrl+S)- デフォルトでPMEPRJ形式"""
84
+ if not self.data.atoms and not self.current_mol:
85
+ self.statusBar().showMessage("Error: Nothing to save.")
86
+ return
87
+ # 非ネイティブ形式(.mol, .sdf, .xyz など)は上書き保存せず、必ず「名前を付けて保存」にする
88
+ native_exts = ['.pmeprj', '.pmeraw']
89
+ if self.current_file_path and any(self.current_file_path.lower().endswith(ext) for ext in native_exts):
90
+ # 既存のPMEPRJ/PMERAWファイルの場合は上書き保存
91
+ try:
92
+ if self.current_file_path.lower().endswith('.pmeraw'):
93
+ # 既存のPMERAWファイルの場合はPMERAW形式で保存
94
+ save_data = self.get_current_state()
95
+ with open(self.current_file_path, 'wb') as f:
96
+ pickle.dump(save_data, f)
97
+ else:
98
+ # PMEPRJ形式で保存
99
+ json_data = self.create_json_data()
100
+ with open(self.current_file_path, 'w', encoding='utf-8') as f:
101
+ json.dump(json_data, f, indent=2, ensure_ascii=False)
102
+
103
+ # 保存成功時に状態をリセット
104
+ self.has_unsaved_changes = False
105
+ self.update_window_title()
106
+
107
+ self.statusBar().showMessage(f"Project saved to {self.current_file_path}")
108
+
109
+ except (OSError, IOError) as e:
110
+ self.statusBar().showMessage(f"File I/O error: {e}")
111
+ except (pickle.PicklingError, TypeError, ValueError) as e:
112
+ self.statusBar().showMessage(f"Data serialization error: {e}")
113
+ except Exception as e:
114
+ self.statusBar().showMessage(f"Error saving project file: {e}")
115
+
116
+ traceback.print_exc()
117
+ else:
118
+ # MOL/SDF/XYZなどは上書き保存せず、必ず「名前を付けて保存」にする
119
+ self.save_project_as()
120
+
121
+
122
+
123
+ def save_project_as(self):
124
+ """名前を付けて保存(Ctrl+Shift+S)- デフォルトでPMEPRJ形式"""
125
+ if not self.data.atoms and not self.current_mol:
126
+ self.statusBar().showMessage("Error: Nothing to save.")
127
+ return
128
+
129
+ try:
130
+ # Determine a sensible default filename based on current file (strip extension)
131
+ default_name = "untitled"
132
+ try:
133
+ if self.current_file_path:
134
+ base = os.path.basename(self.current_file_path)
135
+ default_name = os.path.splitext(base)[0]
136
+ except Exception:
137
+ default_name = "untitled"
138
+
139
+ # Prefer the directory of the currently opened file as default
140
+ default_path = default_name
141
+ try:
142
+ if self.current_file_path:
143
+ default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
144
+ except Exception:
145
+ default_path = default_name
146
+
147
+ file_path, _ = QFileDialog.getSaveFileName(
148
+ self, "Save Project As", default_path,
149
+ "PME Project Files (*.pmeprj);;All Files (*)",
150
+ )
151
+ if not file_path:
152
+ return
153
+
154
+ if not file_path.lower().endswith('.pmeprj'):
155
+ file_path += '.pmeprj'
156
+
157
+ # JSONデータを保存
158
+ json_data = self.create_json_data()
159
+ with open(file_path, 'w', encoding='utf-8') as f:
160
+ json.dump(json_data, f, indent=2, ensure_ascii=False)
161
+
162
+ # 保存成功時に状態をリセット
163
+ self.has_unsaved_changes = False
164
+ # Replace current file with the newly saved file so subsequent saves go to this path
165
+ self.current_file_path = file_path
166
+ self.update_window_title()
167
+ # Mark this state as the last saved state for undo tracking
168
+ try:
169
+ self._saved_state = copy.deepcopy(self.get_current_state())
170
+ except Exception:
171
+ pass
172
+
173
+ self.statusBar().showMessage(f"Project saved to {file_path}")
174
+
175
+ except (OSError, IOError) as e:
176
+ self.statusBar().showMessage(f"File I/O error: {e}")
177
+ except pickle.PicklingError as e:
178
+ self.statusBar().showMessage(f"Data serialization error: {e}")
179
+ except Exception as e:
180
+ self.statusBar().showMessage(f"Error saving project file: {e}")
181
+
182
+ traceback.print_exc()
183
+
184
+
185
+
186
+ def save_raw_data(self):
187
+ if not self.data.atoms and not self.current_mol:
188
+ self.statusBar().showMessage("Error: Nothing to save.")
189
+ return
190
+
191
+ try:
192
+ save_data = self.get_current_state()
193
+ # default filename based on current file
194
+ default_name = "untitled"
195
+ try:
196
+ if self.current_file_path:
197
+ base = os.path.basename(self.current_file_path)
198
+ default_name = os.path.splitext(base)[0]
199
+ except Exception:
200
+ default_name = "untitled"
201
+
202
+ # prefer same directory as current file when available
203
+ default_path = default_name
204
+ try:
205
+ if self.current_file_path:
206
+ default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
207
+ except Exception:
208
+ default_path = default_name
209
+
210
+ file_path, _ = QFileDialog.getSaveFileName(self, "Save Project File", default_path, "Project Files (*.pmeraw);;All Files (*)")
211
+ if not file_path:
212
+ return
213
+
214
+ if not file_path.lower().endswith('.pmeraw'):
215
+ file_path += '.pmeraw'
216
+
217
+ with open(file_path, 'wb') as f:
218
+ pickle.dump(save_data, f)
219
+
220
+ # 保存成功時に状態をリセット
221
+ self.has_unsaved_changes = False
222
+ # Update current file to the newly saved raw file
223
+ self.current_file_path = file_path
224
+ self.update_window_title()
225
+ try:
226
+ self._saved_state = copy.deepcopy(self.get_current_state())
227
+ except Exception:
228
+ pass
229
+
230
+ self.statusBar().showMessage(f"Project saved to {file_path}")
231
+
232
+ except (OSError, IOError) as e:
233
+ self.statusBar().showMessage(f"File I/O error: {e}")
234
+ except pickle.PicklingError as e:
235
+ self.statusBar().showMessage(f"Data serialization error: {e}")
236
+ except Exception as e:
237
+ self.statusBar().showMessage(f"Error saving project file: {e}")
238
+
239
+ traceback.print_exc()
240
+
241
+
242
+
243
+
244
+ def load_raw_data(self, file_path=None):
245
+ if not file_path:
246
+ file_path, _ = QFileDialog.getOpenFileName(self, "Open Project File", "", "Project Files (*.pmeraw);;All Files (*)")
247
+ if not file_path:
248
+ return
249
+
250
+ try:
251
+ with open(file_path, 'rb') as f:
252
+ loaded_data = pickle.load(f)
253
+ self.restore_ui_for_editing()
254
+ self.set_state_from_data(loaded_data)
255
+
256
+ # ファイル読み込み時に状態をリセット
257
+ self.reset_undo_stack()
258
+ self.has_unsaved_changes = False
259
+ self.current_file_path = file_path
260
+ self.update_window_title()
261
+ try:
262
+ self._saved_state= copy.deepcopy(self.et_current_state())
263
+ except Exception:
264
+ pass
265
+
266
+ self.statusBar.showMessage(f"Project loaded from {file_path}")
267
+
268
+ QTimer.singleShot(0, self.fit_to_view)
269
+
270
+ except FileNotFoundError:
271
+ self.statusBar().showMessage(f"File not found: {file_path}")
272
+ except (OSError, IOError) as e:
273
+ self.statusBar().showMessage(f"File I/O error: {e}")
274
+ except pickle.UnpicklingError as e:
275
+ self.statusBar().showMessage(f"Invalid project file format: {e}")
276
+ except Exception as e:
277
+ self.statusBar().showMessage(f"Error loading project file: {e}")
278
+
279
+ traceback.print_exc()
280
+
281
+
282
+
283
+ def save_as_json(self):
284
+ """PMEJSONファイル形式で保存 (3D MOL情報含む)"""
285
+ if not self.data.atoms and not self.current_mol:
286
+ self.statusBar().showMessage("Error: Nothing to save.")
287
+ return
288
+
289
+ try:
290
+ # default filename based on current file
291
+ default_name = "untitled"
292
+ try:
293
+ if self.current_file_path:
294
+ base = os.path.basename(self.current_file_path)
295
+ default_name = os.path.splitext(base)[0]
296
+ except Exception:
297
+ default_name = "untitled"
298
+
299
+ # prefer same directory as current file when available
300
+ default_path = default_name
301
+ try:
302
+ if self.current_file_path:
303
+ default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
304
+ except Exception:
305
+ default_path = default_name
306
+
307
+ file_path, _ = QFileDialog.getSaveFileName(
308
+ self, "Save as PME Project", default_path,
309
+ "PME Project Files (*.pmeprj);;All Files (*)",
310
+ )
311
+ if not file_path:
312
+ return
313
+
314
+ if not file_path.lower().endswith('.pmeprj'):
315
+ file_path += '.pmeprj'
316
+
317
+ # JSONデータを作成
318
+ json_data = self.create_json_data()
319
+
320
+ # JSON形式で保存(美しい整形付き)
321
+ with open(file_path, 'w', encoding='utf-8') as f:
322
+ json.dump(json_data, f, indent=2, ensure_ascii=False)
323
+
324
+ # 保存成功時に状態をリセット
325
+ self.has_unsaved_changes = False
326
+ # Replace current file with the newly saved PME Project
327
+ self.current_file_path = file_path
328
+ self.update_window_title()
329
+
330
+ self.statusBar().showMessage(f"PME Project saved to {file_path}")
331
+
332
+ except (OSError, IOError) as e:
333
+ self.statusBar().showMessage(f"File I/O error: {e}")
334
+ except (TypeError, ValueError) as e:
335
+ self.statusBar().showMessage(f"JSON serialization error: {e}")
336
+ except Exception as e:
337
+ self.statusBar().showMessage(f"Error saving PME Project file: {e}")
338
+
339
+ traceback.print_exc()
340
+
341
+
342
+
343
+ def load_json_data(self, file_path=None):
344
+ """PME Projectファイル形式を読み込み"""
345
+ if not file_path:
346
+ file_path, _ = QFileDialog.getOpenFileName(
347
+ self, "Open PME Project File", "",
348
+ "PME Project Files (*.pmeprj);;All Files (*)",
349
+ )
350
+ if not file_path:
351
+ return
352
+
353
+ try:
354
+ with open(file_path, 'r', encoding='utf-8') as f:
355
+ json_data = json.load(f)
356
+
357
+ # フォーマット検証
358
+ if json_data.get("format") != "PME Project":
359
+ QMessageBox.warning(
360
+ self, "Invalid Format",
361
+ "This file is not a valid PME Project format."
362
+ )
363
+ return
364
+
365
+ # バージョン確認
366
+ file_version = json_data.get("version", "1.0")
367
+ if file_version != "1.0":
368
+ QMessageBox.information(
369
+ self, "Version Notice",
370
+ f"This file was created with PME Project version {file_version}.\n"
371
+ "Loading will be attempted but some features may not work correctly."
372
+ )
373
+
374
+ self.restore_ui_for_editing()
375
+ self.load_from_json_data(json_data)
376
+ # ファイル読み込み時に状態をリセット
377
+ self.reset_undo_stack()
378
+ self.has_unsaved_changes = False
379
+ self.current_file_path = file_path
380
+ self.update_window_title()
381
+
382
+ self.statusBar().showMessage(f"PME Project loaded from {file_path}")
383
+
384
+ QTimer.singleShot(0, self.fit_to_view)
385
+
386
+ except FileNotFoundError:
387
+ self.statusBar().showMessage(f"File not found: {file_path}")
388
+ except json.JSONDecodeError as e:
389
+ self.statusBar().showMessage(f"Invalid JSON format: {e}")
390
+ except (OSError, IOError) as e:
391
+ self.statusBar().showMessage(f"File I/O error: {e}")
392
+ except Exception as e:
393
+ self.statusBar().showMessage(f"Error loading PME Project file: {e}")
394
+
395
+ traceback.print_exc()
396
+
397
+
398
+
399
+ def open_project_file(self, file_path=None):
400
+ """プロジェクトファイルを開く(.pmeprjと.pmerawの両方に対応)"""
401
+ # Check for unsaved changes before opening a new project file.
402
+ # Previously this function opened .pmeprj/.pmeraw without prompting the
403
+ # user to save current unsaved work. Ensure we honor the global
404
+ # unsaved-change check like other loaders (SMILES/MOL/etc.).
405
+ if not self.check_unsaved_changes():
406
+ return
407
+ if not file_path:
408
+ file_path, _ = QFileDialog.getOpenFileName(
409
+ self, "Open Project File", "",
410
+ "PME Project Files (*.pmeprj);;PME Raw Files (*.pmeraw);;All Files (*)",
411
+ )
412
+ if not file_path:
413
+ return
414
+
415
+ # 拡張子に応じて適切な読み込み関数を呼び出し
416
+ if file_path.lower().endswith('.pmeprj'):
417
+ self.load_json_data(file_path)
418
+ elif file_path.lower().endswith('.pmeraw'):
419
+ self.load_raw_data(file_path)
420
+ else:
421
+ # 拡張子不明の場合はJSONとして試行
422
+ try:
423
+ self.load_json_data(file_path)
424
+ except:
425
+ try:
426
+ self.load_raw_data(file_path)
427
+ except:
428
+ self.statusBar().showMessage("Error: Unable to determine file format.")
429
+
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ main_window_string_importers.py
6
+ MainWindow (main_window.py) から分離されたモジュール
7
+ 機能クラス: MainWindowStringImporters
8
+ """
9
+
10
+
11
+ import traceback
12
+
13
+
14
+ # RDKit imports (explicit to satisfy flake8 and used features)
15
+ from rdkit import Chem
16
+ from rdkit.Chem import AllChem
17
+ try:
18
+ pass
19
+ except Exception:
20
+ pass
21
+
22
+ # PyQt6 Modules
23
+ from PyQt6.QtWidgets import (
24
+ QInputDialog
25
+ )
26
+
27
+
28
+
29
+ from PyQt6.QtCore import (
30
+ QPointF, QTimer
31
+ )
32
+
33
+
34
+ # Use centralized Open Babel availability from package-level __init__
35
+ # Use per-package modules availability (local __init__).
36
+ try:
37
+ from . import OBABEL_AVAILABLE
38
+ except Exception:
39
+ from modules import OBABEL_AVAILABLE
40
+ # Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
41
+ if OBABEL_AVAILABLE:
42
+ try:
43
+ from openbabel import pybel
44
+ except Exception:
45
+ # If import fails here, disable OBABEL locally; avoid raising
46
+ pybel = None
47
+ OBABEL_AVAILABLE = False
48
+ print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
49
+ else:
50
+ pybel = None
51
+
52
+ # Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
53
+ # allows safely detecting C++ wrapper objects that have been deleted. Import
54
+ # it once at module import time and expose a small, robust wrapper so callers
55
+ # can avoid re-importing sip repeatedly and so we centralize exception
56
+ # handling (this reduces crash risk during teardown and deletion operations).
57
+ try:
58
+ import sip as _sip # type: ignore
59
+ _sip_isdeleted = getattr(_sip, 'isdeleted', None)
60
+ except Exception:
61
+ _sip = None
62
+ _sip_isdeleted = None
63
+
64
+ try:
65
+ # package relative imports (preferred when running as `python -m moleditpy`)
66
+ pass
67
+ except Exception:
68
+ # Fallback to absolute imports for script-style execution
69
+ pass
70
+
71
+
72
+ # --- クラス定義 ---
73
+ class MainWindowStringImporters(object):
74
+ """ main_window.py から分離された機能クラス """
75
+
76
+ def __init__(self, main_window):
77
+ """ クラスの初期化 """
78
+ self.mw = main_window
79
+
80
+
81
+ def import_smiles_dialog(self):
82
+ """ユーザーにSMILES文字列の入力を促すダイアログを表示する"""
83
+ smiles, ok = QInputDialog.getText(self, "Import SMILES", "Enter SMILES string:")
84
+ if ok and smiles:
85
+ self.load_from_smiles(smiles)
86
+
87
+
88
+
89
+ def import_inchi_dialog(self):
90
+ """ユーザーにInChI文字列の入力を促すダイアログを表示する"""
91
+ inchi, ok = QInputDialog.getText(self, "Import InChI", "Enter InChI string:")
92
+ if ok and inchi:
93
+ self.load_from_inchi(inchi)
94
+
95
+
96
+
97
+ def load_from_smiles(self, smiles_string):
98
+ """SMILES文字列から分子を読み込み、2Dエディタに表示する"""
99
+ try:
100
+ if not self.check_unsaved_changes():
101
+ return # ユーザーがキャンセルした場合は何もしない
102
+
103
+ cleaned_smiles = smiles_string.strip()
104
+
105
+ mol = Chem.MolFromSmiles(cleaned_smiles)
106
+ if mol is None:
107
+ if not cleaned_smiles:
108
+ raise ValueError("SMILES string was empty.")
109
+ raise ValueError("Invalid SMILES string.")
110
+
111
+ AllChem.Compute2DCoords(mol)
112
+ Chem.Kekulize(mol)
113
+
114
+ AllChem.AssignStereochemistry(mol, cleanIt=True, force=True)
115
+ conf = mol.GetConformer()
116
+ AllChem.WedgeMolBonds(mol, conf)
117
+
118
+ self.restore_ui_for_editing()
119
+ self.clear_2d_editor(push_to_undo=False)
120
+ self.current_mol = None
121
+ self.plotter.clear()
122
+ self.analysis_action.setEnabled(False)
123
+
124
+ conf = mol.GetConformer()
125
+ SCALE_FACTOR = 50.0
126
+
127
+ view_center = self.view_2d.mapToScene(self.view_2d.viewport().rect().center())
128
+ positions = [conf.GetAtomPosition(i) for i in range(mol.GetNumAtoms())]
129
+ mol_center_x = sum(p.x for p in positions) / len(positions) if positions else 0.0
130
+ mol_center_y = sum(p.y for p in positions) / len(positions) if positions else 0.0
131
+
132
+ rdkit_idx_to_my_id = {}
133
+ for i in range(mol.GetNumAtoms()):
134
+ atom = mol.GetAtomWithIdx(i)
135
+ pos = conf.GetAtomPosition(i)
136
+ charge = atom.GetFormalCharge()
137
+
138
+ relative_x = pos.x - mol_center_x
139
+ relative_y = pos.y - mol_center_y
140
+
141
+ scene_x = (relative_x * SCALE_FACTOR) + view_center.x()
142
+ scene_y = (-relative_y * SCALE_FACTOR) + view_center.y()
143
+
144
+ atom_id = self.scene.create_atom(atom.GetSymbol(), QPointF(scene_x, scene_y), charge=charge)
145
+ rdkit_idx_to_my_id[i] = atom_id
146
+
147
+
148
+ for bond in mol.GetBonds():
149
+ b_idx, e_idx = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
150
+ b_type = bond.GetBondTypeAsDouble()
151
+ b_dir = bond.GetBondDir()
152
+ stereo = 0
153
+ # 単結合の立体
154
+ if b_dir == Chem.BondDir.BEGINWEDGE:
155
+ stereo = 1 # Wedge
156
+ elif b_dir == Chem.BondDir.BEGINDASH:
157
+ stereo = 2 # Dash
158
+ # 二重結合のE/Z
159
+ if bond.GetBondType() == Chem.BondType.DOUBLE:
160
+ if bond.GetStereo() == Chem.BondStereo.STEREOZ:
161
+ stereo = 3 # Z
162
+ elif bond.GetStereo() == Chem.BondStereo.STEREOE:
163
+ stereo = 4 # E
164
+
165
+ if b_idx in rdkit_idx_to_my_id and e_idx in rdkit_idx_to_my_id:
166
+ a1_id, a2_id = rdkit_idx_to_my_id[b_idx], rdkit_idx_to_my_id[e_idx]
167
+ a1_item = self.data.atoms[a1_id]['item']
168
+ a2_item = self.data.atoms[a2_id]['item']
169
+ self.scene.create_bond(a1_item, a2_item, bond_order=int(b_type), bond_stereo=stereo)
170
+
171
+ self.statusBar().showMessage("Successfully loaded from SMILES.")
172
+ self.reset_undo_stack()
173
+ self.has_unsaved_changes = False
174
+ self.update_window_title()
175
+ QTimer.singleShot(0, self.fit_to_view)
176
+
177
+ except ValueError as e:
178
+ self.statusBar().showMessage(f"Invalid SMILES: {e}")
179
+ except Exception as e:
180
+ self.statusBar().showMessage(f"Error loading from SMILES: {e}")
181
+
182
+ traceback.print_exc()
183
+
184
+
185
+
186
+ def load_from_inchi(self, inchi_string):
187
+ """InChI文字列から分子を読み込み、2Dエディタに表示する"""
188
+ try:
189
+ if not self.check_unsaved_changes():
190
+ return # ユーザーがキャンセルした場合は何もしない
191
+ cleaned_inchi = inchi_string.strip()
192
+
193
+ mol = Chem.MolFromInchi(cleaned_inchi)
194
+ if mol is None:
195
+ if not cleaned_inchi:
196
+ raise ValueError("InChI string was empty.")
197
+ raise ValueError("Invalid InChI string.")
198
+
199
+ AllChem.Compute2DCoords(mol)
200
+ Chem.Kekulize(mol)
201
+
202
+ AllChem.AssignStereochemistry(mol, cleanIt=True, force=True)
203
+ conf = mol.GetConformer()
204
+ AllChem.WedgeMolBonds(mol, conf)
205
+
206
+ self.restore_ui_for_editing()
207
+ self.clear_2d_editor(push_to_undo=False)
208
+ self.current_mol = None
209
+ self.plotter.clear()
210
+ self.analysis_action.setEnabled(False)
211
+
212
+ conf = mol.GetConformer()
213
+ SCALE_FACTOR = 50.0
214
+
215
+ view_center = self.view_2d.mapToScene(self.view_2d.viewport().rect().center())
216
+ positions = [conf.GetAtomPosition(i) for i in range(mol.GetNumAtoms())]
217
+ mol_center_x = sum(p.x for p in positions) / len(positions) if positions else 0.0
218
+ mol_center_y = sum(p.y for p in positions) / len(positions) if positions else 0.0
219
+
220
+ rdkit_idx_to_my_id = {}
221
+ for i in range(mol.GetNumAtoms()):
222
+ atom = mol.GetAtomWithIdx(i)
223
+ pos = conf.GetAtomPosition(i)
224
+ charge = atom.GetFormalCharge()
225
+
226
+ relative_x = pos.x - mol_center_x
227
+ relative_y = pos.y - mol_center_y
228
+
229
+ scene_x = (relative_x * SCALE_FACTOR) + view_center.x()
230
+ scene_y = (-relative_y * SCALE_FACTOR) + view_center.y()
231
+
232
+ atom_id = self.scene.create_atom(atom.GetSymbol(), QPointF(scene_x, scene_y), charge=charge)
233
+ rdkit_idx_to_my_id[i] = atom_id
234
+
235
+ for bond in mol.GetBonds():
236
+ b_idx, e_idx = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
237
+ b_type = bond.GetBondTypeAsDouble()
238
+ b_dir = bond.GetBondDir()
239
+ stereo = 0
240
+ # 単結合の立体
241
+ if b_dir == Chem.BondDir.BEGINWEDGE:
242
+ stereo = 1 # Wedge
243
+ elif b_dir == Chem.BondDir.BEGINDASH:
244
+ stereo = 2 # Dash
245
+ # 二重結合のE/Z
246
+ if bond.GetBondType() == Chem.BondType.DOUBLE:
247
+ if bond.GetStereo() == Chem.BondStereo.STEREOZ:
248
+ stereo = 3 # Z
249
+ elif bond.GetStereo() == Chem.BondStereo.STEREOE:
250
+ stereo = 4 # E
251
+
252
+ if b_idx in rdkit_idx_to_my_id and e_idx in rdkit_idx_to_my_id:
253
+ a1_id, a2_id = rdkit_idx_to_my_id[b_idx], rdkit_idx_to_my_id[e_idx]
254
+ a1_item = self.data.atoms[a1_id]['item']
255
+ a2_item = self.data.atoms[a2_id]['item']
256
+ self.scene.create_bond(a1_item, a2_item, bond_order=int(b_type), bond_stereo=stereo)
257
+
258
+ self.statusBar().showMessage("Successfully loaded from InChI.")
259
+ self.reset_undo_stack()
260
+ self.has_unsaved_changes = False
261
+ self.update_window_title()
262
+ QTimer.singleShot(0, self.fit_to_view)
263
+
264
+ except ValueError as e:
265
+ self.statusBar().showMessage(f"Invalid InChI: {e}")
266
+ except Exception as e:
267
+ self.statusBar().showMessage(f"Error loading from InChI: {e}")
268
+
269
+ traceback.print_exc()
270
+