MoleditPy-linux 2.2.4__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 (58) 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 +293 -0
  7. moleditpy_linux/modules/alignment_dialog.py +273 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/icon.icns +0 -0
  11. moleditpy_linux/modules/assets/icon.ico +0 -0
  12. moleditpy_linux/modules/assets/icon.png +0 -0
  13. moleditpy_linux/modules/atom_item.py +348 -0
  14. moleditpy_linux/modules/bond_item.py +406 -0
  15. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  16. moleditpy_linux/modules/calculation_worker.py +766 -0
  17. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  18. moleditpy_linux/modules/constants.py +88 -0
  19. moleditpy_linux/modules/constrained_optimization_dialog.py +679 -0
  20. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  21. moleditpy_linux/modules/custom_qt_interactor.py +59 -0
  22. moleditpy_linux/modules/dialog3_d_picking_mixin.py +108 -0
  23. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  24. moleditpy_linux/modules/main_window.py +842 -0
  25. moleditpy_linux/modules/main_window_app_state.py +780 -0
  26. moleditpy_linux/modules/main_window_compute.py +1242 -0
  27. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  28. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  29. moleditpy_linux/modules/main_window_edit_actions.py +1455 -0
  30. moleditpy_linux/modules/main_window_export.py +806 -0
  31. moleditpy_linux/modules/main_window_main_init.py +2006 -0
  32. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  33. moleditpy_linux/modules/main_window_project_io.py +434 -0
  34. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  35. moleditpy_linux/modules/main_window_ui_manager.py +606 -0
  36. moleditpy_linux/modules/main_window_view_3d.py +1531 -0
  37. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  38. moleditpy_linux/modules/mirror_dialog.py +122 -0
  39. moleditpy_linux/modules/molecular_data.py +302 -0
  40. moleditpy_linux/modules/molecule_scene.py +2000 -0
  41. moleditpy_linux/modules/move_group_dialog.py +598 -0
  42. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  43. moleditpy_linux/modules/planarize_dialog.py +221 -0
  44. moleditpy_linux/modules/plugin_interface.py +195 -0
  45. moleditpy_linux/modules/plugin_manager.py +309 -0
  46. moleditpy_linux/modules/plugin_manager_window.py +221 -0
  47. moleditpy_linux/modules/settings_dialog.py +1149 -0
  48. moleditpy_linux/modules/template_preview_item.py +157 -0
  49. moleditpy_linux/modules/template_preview_view.py +74 -0
  50. moleditpy_linux/modules/translation_dialog.py +365 -0
  51. moleditpy_linux/modules/user_template_dialog.py +692 -0
  52. moleditpy_linux/modules/zoomable_view.py +129 -0
  53. moleditpy_linux-2.2.4.dist-info/METADATA +936 -0
  54. moleditpy_linux-2.2.4.dist-info/RECORD +58 -0
  55. moleditpy_linux-2.2.4.dist-info/WHEEL +5 -0
  56. moleditpy_linux-2.2.4.dist-info/entry_points.txt +2 -0
  57. moleditpy_linux-2.2.4.dist-info/licenses/LICENSE +674 -0
  58. moleditpy_linux-2.2.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,406 @@
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 QGraphicsItem, QGraphicsScene
14
+
15
+ from PyQt6.QtGui import (
16
+ QPen, QBrush, QColor, QFont, QPolygonF,
17
+ QPainterPath, QPainterPathStroker, QFontMetricsF
18
+ )
19
+
20
+ from PyQt6.QtCore import (
21
+ Qt, QPointF, QRectF, QLineF
22
+ )
23
+
24
+ try:
25
+ from .constants import (
26
+ EZ_LABEL_BOX_SIZE, EZ_LABEL_TEXT_OUTLINE, EZ_LABEL_MARGIN,
27
+ BOND_OFFSET, FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
28
+ HOVER_PEN_WIDTH, DESIRED_BOND_PIXEL_WIDTH,
29
+ )
30
+ except Exception:
31
+ from modules.constants import (
32
+ EZ_LABEL_BOX_SIZE, EZ_LABEL_TEXT_OUTLINE, EZ_LABEL_MARGIN,
33
+ BOND_OFFSET, FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
34
+ HOVER_PEN_WIDTH, DESIRED_BOND_PIXEL_WIDTH,
35
+ )
36
+
37
+ class BondItem(QGraphicsItem):
38
+
39
+ def get_ez_label_rect(self):
40
+ """E/Zラベルの描画範囲(シーン座標)を返す。ラベルが無い場合はNone。"""
41
+ if self.order != 2 or self.stereo not in [3, 4]:
42
+ return None
43
+ line = self.get_line_in_local_coords()
44
+ center = line.center()
45
+ label_width = EZ_LABEL_BOX_SIZE
46
+ label_height = EZ_LABEL_BOX_SIZE
47
+ label_rect = QRectF(center.x() - label_width/2, center.y() - label_height/2, label_width, label_height)
48
+ # シーン座標に変換
49
+ return self.mapToScene(label_rect).boundingRect()
50
+ def set_stereo(self, new_stereo):
51
+ try:
52
+ # ラベルを消す場合は、消す前のboundingRectをscene().invalidateで強制的に無効化
53
+ if new_stereo == 0 and self.stereo in [3, 4] and self.scene():
54
+ rect = self.mapToScene(self.boundingRect()).boundingRect()
55
+ self.scene().invalidate(rect, QGraphicsScene.SceneLayer.BackgroundLayer | QGraphicsScene.SceneLayer.ForegroundLayer)
56
+
57
+ self.prepareGeometryChange()
58
+ self.stereo = new_stereo
59
+ self.update()
60
+
61
+ if self.scene() and self.scene().views():
62
+ try:
63
+ self.scene().views()[0].viewport().update()
64
+ except (IndexError, RuntimeError):
65
+ # Handle case where views are being destroyed
66
+ pass
67
+
68
+ except Exception as e:
69
+ print(f"Error in BondItem.set_stereo: {e}")
70
+ # Continue without crashing
71
+ self.stereo = new_stereo
72
+
73
+ def set_order(self, new_order):
74
+ self.prepareGeometryChange()
75
+ self.order = new_order
76
+ self.update()
77
+ if self.scene() and self.scene().views():
78
+ self.scene().views()[0].viewport().update()
79
+ def __init__(self, atom1_item, atom2_item, order=1, stereo=0):
80
+ super().__init__()
81
+ # Validate input parameters
82
+ if atom1_item is None or atom2_item is None:
83
+ raise ValueError("BondItem requires non-None atom items")
84
+ self.atom1, self.atom2, self.order, self.stereo = atom1_item, atom2_item, order, stereo
85
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
86
+ self.pen = QPen(Qt.GlobalColor.black, 2)
87
+ self.setZValue(0)
88
+ self.update_position()
89
+ self.setAcceptHoverEvents(True)
90
+ self.hovered = False
91
+
92
+
93
+ def get_line_in_local_coords(self):
94
+ if self.atom1 is None or self.atom2 is None:
95
+ return QLineF(0, 0, 0, 0)
96
+ try:
97
+ p2 = self.mapFromItem(self.atom2, 0, 0)
98
+ return QLineF(QPointF(0, 0), p2)
99
+ except (RuntimeError, TypeError):
100
+ # Handle case where atoms are deleted from scene
101
+ return QLineF(0, 0, 0, 0)
102
+
103
+ def boundingRect(self):
104
+ try:
105
+ line = self.get_line_in_local_coords()
106
+ except Exception:
107
+ line = QLineF(0, 0, 0, 0)
108
+ bond_offset = globals().get('BOND_OFFSET', 2)
109
+ extra = (getattr(self, 'order', 1) - 1) * bond_offset + 20
110
+ rect = QRectF(line.p1(), line.p2()).normalized().adjusted(-extra, -extra, extra, extra)
111
+
112
+ # E/Zラベルの描画範囲も考慮して拡張(QFontMetricsFで正確に)
113
+ if self.order == 2 and self.stereo in [3, 4]:
114
+ font = QFont(FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD)
115
+ font.setItalic(True)
116
+ text = "Z" if self.stereo == 3 else "E"
117
+ fm = QFontMetricsF(font)
118
+ text_rect = fm.boundingRect(text)
119
+ outline = EZ_LABEL_TEXT_OUTLINE # 輪郭の太さ分
120
+ margin = EZ_LABEL_MARGIN # 追加余白
121
+ center = line.center()
122
+ label_rect = QRectF(center.x() - text_rect.width()/2 - outline - margin,
123
+ center.y() - text_rect.height()/2 - outline - margin,
124
+ text_rect.width() + 2*outline + 2*margin,
125
+ text_rect.height() + 2*outline + 2*margin)
126
+ rect = rect.united(label_rect)
127
+ return rect
128
+
129
+ def shape(self):
130
+ path = QPainterPath()
131
+ try:
132
+ line = self.get_line_in_local_coords()
133
+ except Exception:
134
+ return path
135
+ if line.length() == 0:
136
+ return path
137
+
138
+ scene = self.scene()
139
+ if not scene or not scene.views():
140
+ return super().shape()
141
+
142
+ view = scene.views()[0]
143
+ scale = view.transform().m11()
144
+
145
+ scene_width = DESIRED_BOND_PIXEL_WIDTH / scale
146
+
147
+ stroker = QPainterPathStroker()
148
+ stroker.setWidth(scene_width)
149
+ stroker.setCapStyle(Qt.PenCapStyle.RoundCap)
150
+ stroker.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
151
+
152
+ center_line_path = QPainterPath(line.p1())
153
+ center_line_path.lineTo(line.p2())
154
+
155
+ return stroker.createStroke(center_line_path)
156
+
157
+ def paint(self, painter, option, widget):
158
+ if self.atom1 is None or self.atom2 is None:
159
+ return
160
+ line = self.get_line_in_local_coords()
161
+ if line.length() == 0: return
162
+
163
+ # --- 1. 選択状態に応じてペンとブラシを準備 ---
164
+ if self.isSelected():
165
+ selection_color = QColor("blue")
166
+ painter.setPen(QPen(selection_color, 3))
167
+ painter.setBrush(QBrush(selection_color))
168
+ else:
169
+ # Allow bond color override from app settings (2D color)
170
+ try:
171
+ sc = self.scene()
172
+ if sc is not None and hasattr(sc, 'window') and sc.window is not None:
173
+ bond_hex = sc.window.settings.get('bond_color', '#222222')
174
+ bond_color = QColor(bond_hex)
175
+ painter.setPen(QPen(bond_color, 2))
176
+ else:
177
+ painter.setPen(self.pen)
178
+ except Exception:
179
+ painter.setPen(self.pen)
180
+ painter.setBrush(QBrush(Qt.GlobalColor.black))
181
+
182
+ # --- 立体化学 (Wedge/Dash) の描画 ---
183
+ if self.order == 1 and self.stereo in [1, 2]:
184
+ vec = line.unitVector()
185
+ normal = vec.normalVector()
186
+ p1 = line.p1() + vec.p2() * 5
187
+ p2 = line.p2() - vec.p2() * 5
188
+
189
+ if self.stereo == 1: # Wedge (くさび形)
190
+ offset = QPointF(normal.dx(), normal.dy()) * 6.0
191
+ poly = QPolygonF([p1, p2 + offset, p2 - offset])
192
+ painter.drawPolygon(poly)
193
+
194
+ elif self.stereo == 2: # Dash (破線)
195
+ painter.save()
196
+ if not self.isSelected():
197
+ pen = painter.pen()
198
+ pen.setWidthF(2.5)
199
+ painter.setPen(pen)
200
+
201
+ num_dashes = 8
202
+ for i in range(num_dashes + 1):
203
+ t = i / num_dashes
204
+ start_pt = p1 * (1 - t) + p2 * t
205
+ width = 12.0 * t
206
+ offset = QPointF(normal.dx(), normal.dy()) * width / 2.0
207
+ painter.drawLine(start_pt - offset, start_pt + offset)
208
+ painter.restore()
209
+
210
+ # --- 通常の結合 (単/二重/三重) の描画 ---
211
+ else:
212
+ if self.order == 1:
213
+ painter.drawLine(line)
214
+ else:
215
+ v = line.unitVector().normalVector()
216
+ offset = QPointF(v.dx(), v.dy()) * BOND_OFFSET
217
+
218
+ if self.order == 2:
219
+ # 環構造かどうかを判定し、描画方法を変更
220
+ is_in_ring = False
221
+ ring_center = None
222
+
223
+ try:
224
+ # シーンからRDKit分子を取得
225
+ sc = self.scene()
226
+ if sc and hasattr(sc, 'window') and sc.window:
227
+ # 2DデータからRDKit分子を生成
228
+ mol = sc.window.data.to_rdkit_mol(use_2d_stereo=False)
229
+ if mol:
230
+ # この結合に対応するRDKitボンドを探す
231
+ atom1_id = self.atom1.atom_id
232
+ atom2_id = self.atom2.atom_id
233
+
234
+ # RDKitインデックスを取得
235
+ rdkit_idx1 = None
236
+ rdkit_idx2 = None
237
+ for atom in mol.GetAtoms():
238
+ if atom.HasProp("_original_atom_id"):
239
+ orig_id = atom.GetIntProp("_original_atom_id")
240
+ if orig_id == atom1_id:
241
+ rdkit_idx1 = atom.GetIdx()
242
+ elif orig_id == atom2_id:
243
+ rdkit_idx2 = atom.GetIdx()
244
+
245
+ if rdkit_idx1 is not None and rdkit_idx2 is not None:
246
+ bond = mol.GetBondBetweenAtoms(rdkit_idx1, rdkit_idx2)
247
+ if bond and bond.IsInRing():
248
+ is_in_ring = True
249
+ # 環の中心を計算(この結合を含む最小環)
250
+ from rdkit import Chem
251
+ ring_info = mol.GetRingInfo()
252
+ for ring in ring_info.AtomRings():
253
+ if rdkit_idx1 in ring and rdkit_idx2 in ring:
254
+ # 環の原子位置の平均を計算
255
+ ring_positions = []
256
+ for atom_idx in ring:
257
+ # 対応するエディタ側の原子を探す
258
+ rdkit_atom = mol.GetAtomWithIdx(atom_idx)
259
+ if rdkit_atom.HasProp("_original_atom_id"):
260
+ editor_atom_id = rdkit_atom.GetIntProp("_original_atom_id")
261
+ if editor_atom_id in sc.window.data.atoms:
262
+ atom_item = sc.window.data.atoms[editor_atom_id]['item']
263
+ if atom_item:
264
+ ring_positions.append(atom_item.pos())
265
+
266
+ if ring_positions:
267
+ # 環の中心を計算
268
+ center_x = sum(p.x() for p in ring_positions) / len(ring_positions)
269
+ center_y = sum(p.y() for p in ring_positions) / len(ring_positions)
270
+ ring_center = QPointF(center_x, center_y)
271
+ break
272
+ except Exception as e:
273
+ # エラーが発生した場合は通常の描画にフォールバック
274
+ is_in_ring = False
275
+
276
+ v = line.unitVector().normalVector()
277
+ offset = QPointF(v.dx(), v.dy()) * BOND_OFFSET
278
+
279
+ if is_in_ring and ring_center:
280
+ # 環構造: 1本の中心線(単結合位置) + 1本の短い内側線
281
+ # 結合の中心から環の中心への方向を計算
282
+ bond_center = line.center()
283
+
284
+ # ローカル座標系での環中心方向
285
+ local_ring_center = self.mapFromScene(ring_center)
286
+ local_bond_center = line.center()
287
+ inward_vec = local_ring_center - local_bond_center
288
+
289
+ # offsetとinward_vecの内積で内側を判定
290
+ if QPointF.dotProduct(offset, inward_vec) > 0:
291
+ # offsetが内側方向(2倍のオフセット)
292
+ inner_offset = offset * 2
293
+ else:
294
+ # -offsetが内側方向(2倍のオフセット)
295
+ inner_offset = -offset * 2
296
+
297
+ # 中心線を描画(単結合と同じ位置)
298
+ painter.drawLine(line)
299
+
300
+ # 内側の短い線を描画(80%の長さ)
301
+ inner_line = line.translated(inner_offset)
302
+ shorten_factor = 0.8
303
+ p1 = inner_line.p1()
304
+ p2 = inner_line.p2()
305
+ center = QPointF((p1.x() + p2.x()) / 2, (p1.y() + p2.y()) / 2)
306
+ shortened_p1 = center + (p1 - center) * shorten_factor
307
+ shortened_p2 = center + (p2 - center) * shorten_factor
308
+ painter.drawLine(QLineF(shortened_p1, shortened_p2))
309
+ else:
310
+ # 非環構造: 従来の2本の平行線
311
+ line1 = line.translated(offset)
312
+ line2 = line.translated(-offset)
313
+ painter.drawLine(line1)
314
+ painter.drawLine(line2)
315
+
316
+ # E/Z ラベルの描画処理
317
+ if self.stereo in [3, 4]:
318
+ painter.save() # 現在の描画設定を保存
319
+
320
+ # --- ラベルの設定 ---
321
+ font = QFont(FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD)
322
+ font.setItalic(True)
323
+ text_color = QColor("gray")
324
+ # 輪郭の色を背景色と同じにする(scene()がNoneのときは安全なフォールバックを使う)
325
+ outline_color = None
326
+ try:
327
+ sc = self.scene()
328
+ if sc is not None:
329
+ outline_color = sc.backgroundBrush().color()
330
+ except Exception:
331
+ outline_color = None
332
+ if outline_color is None:
333
+ # デフォルトでは白背景を想定して黒系の輪郭が見やすい
334
+ outline_color = QColor(255, 255, 255)
335
+
336
+ # --- 描画パスの作成 ---
337
+ text = "Z" if self.stereo == 3 else "E"
338
+ path = QPainterPath()
339
+
340
+ # テキストが正確に中央に来るように位置を計算
341
+ fm = QFontMetricsF(font)
342
+ text_rect = fm.boundingRect(text)
343
+ text_rect.moveCenter(line.center())
344
+ path.addText(text_rect.topLeft(), font, text)
345
+
346
+ # --- 輪郭の描画 ---
347
+ stroker = QPainterPathStroker()
348
+ stroker.setWidth(EZ_LABEL_TEXT_OUTLINE) # 輪郭の太さ
349
+ outline_path = stroker.createStroke(path)
350
+
351
+ painter.setBrush(outline_color)
352
+ painter.setPen(Qt.PenStyle.NoPen)
353
+ painter.drawPath(outline_path)
354
+
355
+ # --- 文字本体の描画 ---
356
+ painter.setBrush(text_color)
357
+ painter.setPen(text_color)
358
+ painter.drawPath(path)
359
+
360
+ painter.restore() # 描画設定を元に戻す
361
+
362
+ elif self.order == 3:
363
+ painter.drawLine(line)
364
+ painter.drawLine(line.translated(offset))
365
+ painter.drawLine(line.translated(-offset))
366
+
367
+ # --- 2. ホバー時のエフェクトを上から重ねて描画 ---
368
+ if (not self.isSelected()) and getattr(self, 'hovered', False):
369
+ try:
370
+ # ホバー時のハイライトを太めの半透明な線で描画
371
+ hover_pen = QPen(QColor(144, 238, 144, 180), HOVER_PEN_WIDTH) # LightGreen, 半透明
372
+ hover_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
373
+ painter.setPen(hover_pen)
374
+ painter.drawLine(line)
375
+ except Exception:
376
+ pass
377
+
378
+
379
+
380
+ def update_position(self):
381
+ try:
382
+ self.prepareGeometryChange()
383
+ if self.atom1:
384
+ self.setPos(self.atom1.pos())
385
+ self.update()
386
+ except Exception as e:
387
+ print(f"Error updating bond position: {e}")
388
+ # Continue without crashing
389
+
390
+
391
+ def hoverEnterEvent(self, event):
392
+ scene = self.scene()
393
+ mode = getattr(scene, 'mode', '')
394
+ self.hovered = True
395
+ self.update()
396
+ if self.scene():
397
+ self.scene().set_hovered_item(self)
398
+ super().hoverEnterEvent(event)
399
+
400
+ def hoverLeaveEvent(self, event):
401
+ if self.hovered:
402
+ self.hovered = False
403
+ self.update()
404
+ if self.scene():
405
+ self.scene().set_hovered_item(None)
406
+ super().hoverLeaveEvent(event)