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.
- moleditpy_linux/__init__.py +17 -0
- moleditpy_linux/__main__.py +29 -0
- moleditpy_linux/main.py +37 -0
- moleditpy_linux/modules/__init__.py +41 -0
- moleditpy_linux/modules/about_dialog.py +104 -0
- moleditpy_linux/modules/align_plane_dialog.py +292 -0
- moleditpy_linux/modules/alignment_dialog.py +272 -0
- moleditpy_linux/modules/analysis_window.py +209 -0
- moleditpy_linux/modules/angle_dialog.py +440 -0
- moleditpy_linux/modules/assets/file_icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.icns +0 -0
- moleditpy_linux/modules/assets/icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.png +0 -0
- moleditpy_linux/modules/atom_item.py +395 -0
- moleditpy_linux/modules/bond_item.py +464 -0
- moleditpy_linux/modules/bond_length_dialog.py +380 -0
- moleditpy_linux/modules/calculation_worker.py +766 -0
- moleditpy_linux/modules/color_settings_dialog.py +321 -0
- moleditpy_linux/modules/constants.py +88 -0
- moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
- moleditpy_linux/modules/custom_interactor_style.py +749 -0
- moleditpy_linux/modules/custom_qt_interactor.py +102 -0
- moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
- moleditpy_linux/modules/dihedral_dialog.py +443 -0
- moleditpy_linux/modules/main_window.py +850 -0
- moleditpy_linux/modules/main_window_app_state.py +787 -0
- moleditpy_linux/modules/main_window_compute.py +1242 -0
- moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
- moleditpy_linux/modules/main_window_edit_3d.py +536 -0
- moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
- moleditpy_linux/modules/main_window_export.py +917 -0
- moleditpy_linux/modules/main_window_main_init.py +2100 -0
- moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
- moleditpy_linux/modules/main_window_project_io.py +434 -0
- moleditpy_linux/modules/main_window_string_importers.py +275 -0
- moleditpy_linux/modules/main_window_ui_manager.py +602 -0
- moleditpy_linux/modules/main_window_view_3d.py +1539 -0
- moleditpy_linux/modules/main_window_view_loaders.py +355 -0
- moleditpy_linux/modules/mirror_dialog.py +122 -0
- moleditpy_linux/modules/molecular_data.py +302 -0
- moleditpy_linux/modules/molecule_scene.py +2000 -0
- moleditpy_linux/modules/move_group_dialog.py +600 -0
- moleditpy_linux/modules/periodic_table_dialog.py +84 -0
- moleditpy_linux/modules/planarize_dialog.py +220 -0
- moleditpy_linux/modules/plugin_interface.py +215 -0
- moleditpy_linux/modules/plugin_manager.py +473 -0
- moleditpy_linux/modules/plugin_manager_window.py +274 -0
- moleditpy_linux/modules/settings_dialog.py +1503 -0
- moleditpy_linux/modules/template_preview_item.py +157 -0
- moleditpy_linux/modules/template_preview_view.py +74 -0
- moleditpy_linux/modules/translation_dialog.py +364 -0
- moleditpy_linux/modules/user_template_dialog.py +692 -0
- moleditpy_linux/modules/zoomable_view.py +129 -0
- moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
- moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
- moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
- moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
- moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
- moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,464 @@
|
|
|
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
|
+
|
|
109
|
+
# Get dynamic bond offset (spacing)
|
|
110
|
+
bond_offset = 3.5
|
|
111
|
+
try:
|
|
112
|
+
if self.scene() and hasattr(self.scene(), 'views') and self.scene().views():
|
|
113
|
+
win = self.scene().views()[0].window()
|
|
114
|
+
if win and hasattr(win, 'settings'):
|
|
115
|
+
# Use specific spacing based on bond order
|
|
116
|
+
if getattr(self, 'order', 1) == 3:
|
|
117
|
+
bond_offset = win.settings.get('bond_spacing_triple_2d', 3.5)
|
|
118
|
+
else:
|
|
119
|
+
bond_offset = win.settings.get('bond_spacing_double_2d', 3.5)
|
|
120
|
+
except Exception:
|
|
121
|
+
bond_offset = globals().get('BOND_OFFSET', 3.5)
|
|
122
|
+
|
|
123
|
+
extra = (getattr(self, 'order', 1) - 1) * bond_offset + 20
|
|
124
|
+
rect = QRectF(line.p1(), line.p2()).normalized().adjusted(-extra, -extra, extra, extra)
|
|
125
|
+
|
|
126
|
+
# E/Zラベルの描画範囲も考慮して拡張(QFontMetricsFで正確に)
|
|
127
|
+
if self.order == 2 and self.stereo in [3, 4]:
|
|
128
|
+
font = QFont(FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD)
|
|
129
|
+
font.setItalic(True)
|
|
130
|
+
text = "Z" if self.stereo == 3 else "E"
|
|
131
|
+
fm = QFontMetricsF(font)
|
|
132
|
+
text_rect = fm.boundingRect(text)
|
|
133
|
+
outline = EZ_LABEL_TEXT_OUTLINE # 輪郭の太さ分
|
|
134
|
+
margin = EZ_LABEL_MARGIN # 追加余白
|
|
135
|
+
center = line.center()
|
|
136
|
+
label_rect = QRectF(center.x() - text_rect.width()/2 - outline - margin,
|
|
137
|
+
center.y() - text_rect.height()/2 - outline - margin,
|
|
138
|
+
text_rect.width() + 2*outline + 2*margin,
|
|
139
|
+
text_rect.height() + 2*outline + 2*margin)
|
|
140
|
+
rect = rect.united(label_rect)
|
|
141
|
+
return rect
|
|
142
|
+
|
|
143
|
+
def shape(self):
|
|
144
|
+
path = QPainterPath()
|
|
145
|
+
try:
|
|
146
|
+
line = self.get_line_in_local_coords()
|
|
147
|
+
except Exception:
|
|
148
|
+
return path
|
|
149
|
+
if line.length() == 0:
|
|
150
|
+
return path
|
|
151
|
+
|
|
152
|
+
scene = self.scene()
|
|
153
|
+
if not scene or not scene.views():
|
|
154
|
+
return super().shape()
|
|
155
|
+
|
|
156
|
+
view = scene.views()[0]
|
|
157
|
+
scale = view.transform().m11()
|
|
158
|
+
|
|
159
|
+
# Dynamic bond width
|
|
160
|
+
width_2d = 2.0
|
|
161
|
+
try:
|
|
162
|
+
if view.window() and hasattr(view.window(), 'settings'):
|
|
163
|
+
width_2d = view.window().settings.get('bond_width_2d', 2.0)
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
# Hit area should be roughly closely matched or slightly larger than visual
|
|
168
|
+
# Ensure minimum hit width for usability
|
|
169
|
+
scene_width = max(DESIRED_BOND_PIXEL_WIDTH, width_2d * 10) / scale
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
stroker = QPainterPathStroker()
|
|
173
|
+
stroker.setWidth(scene_width)
|
|
174
|
+
stroker.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
175
|
+
stroker.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
|
176
|
+
|
|
177
|
+
center_line_path = QPainterPath(line.p1())
|
|
178
|
+
center_line_path.lineTo(line.p2())
|
|
179
|
+
|
|
180
|
+
return stroker.createStroke(center_line_path)
|
|
181
|
+
|
|
182
|
+
def paint(self, painter, option, widget):
|
|
183
|
+
if self.atom1 is None or self.atom2 is None:
|
|
184
|
+
return
|
|
185
|
+
line = self.get_line_in_local_coords()
|
|
186
|
+
if line.length() == 0: return
|
|
187
|
+
|
|
188
|
+
# Allow bond color override from app settings (2D color)
|
|
189
|
+
width_2d = 2.0
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
sc = self.scene()
|
|
193
|
+
if sc is not None and hasattr(sc, 'window') and sc.window is not None:
|
|
194
|
+
# Get settings
|
|
195
|
+
settings = sc.window.settings
|
|
196
|
+
|
|
197
|
+
# Width
|
|
198
|
+
width_2d = settings.get('bond_width_2d', 2.0)
|
|
199
|
+
|
|
200
|
+
# Cap Style logic
|
|
201
|
+
cap_style_str = settings.get('bond_cap_style_2d', 'Round')
|
|
202
|
+
cap_style = Qt.PenCapStyle.RoundCap # Default
|
|
203
|
+
|
|
204
|
+
if cap_style_str == 'Flat':
|
|
205
|
+
cap_style = Qt.PenCapStyle.FlatCap
|
|
206
|
+
elif cap_style_str == 'Square':
|
|
207
|
+
cap_style = Qt.PenCapStyle.SquareCap
|
|
208
|
+
|
|
209
|
+
# Color
|
|
210
|
+
if self.isSelected():
|
|
211
|
+
bond_color = QColor("blue") # Selection color
|
|
212
|
+
else:
|
|
213
|
+
bond_hex = settings.get('bond_color_2d', '#222222')
|
|
214
|
+
bond_color = QColor(bond_hex)
|
|
215
|
+
|
|
216
|
+
pen = QPen(bond_color, width_2d)
|
|
217
|
+
pen.setCapStyle(cap_style)
|
|
218
|
+
painter.setPen(pen)
|
|
219
|
+
|
|
220
|
+
else:
|
|
221
|
+
painter.setPen(self.pen)
|
|
222
|
+
except Exception:
|
|
223
|
+
painter.setPen(self.pen)
|
|
224
|
+
|
|
225
|
+
painter.setBrush(QBrush(Qt.GlobalColor.black))
|
|
226
|
+
|
|
227
|
+
# --- 立体化学 (Wedge/Dash) の描画 ---
|
|
228
|
+
if self.order == 1 and self.stereo in [1, 2]:
|
|
229
|
+
vec = line.unitVector()
|
|
230
|
+
normal = vec.normalVector()
|
|
231
|
+
p1 = line.p1() + vec.p2() * 5
|
|
232
|
+
p2 = line.p2() - vec.p2() * 5
|
|
233
|
+
|
|
234
|
+
if self.stereo == 1: # Wedge (くさび形)
|
|
235
|
+
offset = QPointF(normal.dx(), normal.dy()) * 6.0
|
|
236
|
+
poly = QPolygonF([p1, p2 + offset, p2 - offset])
|
|
237
|
+
painter.drawPolygon(poly)
|
|
238
|
+
|
|
239
|
+
elif self.stereo == 2: # Dash (破線)
|
|
240
|
+
painter.save()
|
|
241
|
+
if not self.isSelected():
|
|
242
|
+
pen = painter.pen()
|
|
243
|
+
pen.setWidthF(2.5)
|
|
244
|
+
painter.setPen(pen)
|
|
245
|
+
|
|
246
|
+
num_dashes = 8
|
|
247
|
+
for i in range(num_dashes + 1):
|
|
248
|
+
t = i / num_dashes
|
|
249
|
+
start_pt = p1 * (1 - t) + p2 * t
|
|
250
|
+
width = 12.0 * t
|
|
251
|
+
offset = QPointF(normal.dx(), normal.dy()) * width / 2.0
|
|
252
|
+
painter.drawLine(start_pt - offset, start_pt + offset)
|
|
253
|
+
painter.restore()
|
|
254
|
+
|
|
255
|
+
# --- 通常の結合 (単/二重/三重) の描画 ---
|
|
256
|
+
else:
|
|
257
|
+
if self.order == 1:
|
|
258
|
+
painter.drawLine(line)
|
|
259
|
+
else:
|
|
260
|
+
v = line.unitVector().normalVector()
|
|
261
|
+
# Use dynamic offset
|
|
262
|
+
bond_offset = 3.5
|
|
263
|
+
try:
|
|
264
|
+
sc = self.scene()
|
|
265
|
+
if sc and sc.views() and hasattr(sc.views()[0].window(), 'settings'):
|
|
266
|
+
if self.order == 3:
|
|
267
|
+
bond_offset = sc.views()[0].window().settings.get('bond_spacing_triple_2d', 3.5)
|
|
268
|
+
else:
|
|
269
|
+
bond_offset = sc.views()[0].window().settings.get('bond_spacing_double_2d', 3.5)
|
|
270
|
+
except Exception:
|
|
271
|
+
bond_offset = globals().get('BOND_OFFSET', 3.5)
|
|
272
|
+
|
|
273
|
+
offset = QPointF(v.dx(), v.dy()) * bond_offset
|
|
274
|
+
|
|
275
|
+
if self.order == 2:
|
|
276
|
+
# 環構造かどうかを判定し、描画方法を変更
|
|
277
|
+
is_in_ring = False
|
|
278
|
+
ring_center = None
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
# シーンからRDKit分子を取得
|
|
282
|
+
sc = self.scene()
|
|
283
|
+
if sc and hasattr(sc, 'window') and sc.window:
|
|
284
|
+
# 2DデータからRDKit分子を生成
|
|
285
|
+
mol = sc.window.data.to_rdkit_mol(use_2d_stereo=False)
|
|
286
|
+
if mol:
|
|
287
|
+
# この結合に対応するRDKitボンドを探す
|
|
288
|
+
atom1_id = self.atom1.atom_id
|
|
289
|
+
atom2_id = self.atom2.atom_id
|
|
290
|
+
|
|
291
|
+
# RDKitインデックスを取得
|
|
292
|
+
rdkit_idx1 = None
|
|
293
|
+
rdkit_idx2 = None
|
|
294
|
+
for atom in mol.GetAtoms():
|
|
295
|
+
if atom.HasProp("_original_atom_id"):
|
|
296
|
+
orig_id = atom.GetIntProp("_original_atom_id")
|
|
297
|
+
if orig_id == atom1_id:
|
|
298
|
+
rdkit_idx1 = atom.GetIdx()
|
|
299
|
+
elif orig_id == atom2_id:
|
|
300
|
+
rdkit_idx2 = atom.GetIdx()
|
|
301
|
+
|
|
302
|
+
if rdkit_idx1 is not None and rdkit_idx2 is not None:
|
|
303
|
+
bond = mol.GetBondBetweenAtoms(rdkit_idx1, rdkit_idx2)
|
|
304
|
+
if bond and bond.IsInRing():
|
|
305
|
+
is_in_ring = True
|
|
306
|
+
# 環の中心を計算(この結合を含む最小環)
|
|
307
|
+
from rdkit import Chem
|
|
308
|
+
ring_info = mol.GetRingInfo()
|
|
309
|
+
for ring in ring_info.AtomRings():
|
|
310
|
+
if rdkit_idx1 in ring and rdkit_idx2 in ring:
|
|
311
|
+
# 環の原子位置の平均を計算
|
|
312
|
+
ring_positions = []
|
|
313
|
+
for atom_idx in ring:
|
|
314
|
+
# 対応するエディタ側の原子を探す
|
|
315
|
+
rdkit_atom = mol.GetAtomWithIdx(atom_idx)
|
|
316
|
+
if rdkit_atom.HasProp("_original_atom_id"):
|
|
317
|
+
editor_atom_id = rdkit_atom.GetIntProp("_original_atom_id")
|
|
318
|
+
if editor_atom_id in sc.window.data.atoms:
|
|
319
|
+
atom_item = sc.window.data.atoms[editor_atom_id]['item']
|
|
320
|
+
if atom_item:
|
|
321
|
+
ring_positions.append(atom_item.pos())
|
|
322
|
+
|
|
323
|
+
if ring_positions:
|
|
324
|
+
# 環の中心を計算
|
|
325
|
+
center_x = sum(p.x() for p in ring_positions) / len(ring_positions)
|
|
326
|
+
center_y = sum(p.y() for p in ring_positions) / len(ring_positions)
|
|
327
|
+
ring_center = QPointF(center_x, center_y)
|
|
328
|
+
break
|
|
329
|
+
except Exception as e:
|
|
330
|
+
# エラーが発生した場合は通常の描画にフォールバック
|
|
331
|
+
is_in_ring = False
|
|
332
|
+
|
|
333
|
+
v = line.unitVector().normalVector()
|
|
334
|
+
# Re-calculate offset in case loop variable scope issue, though strictly not needed if offset defined above works
|
|
335
|
+
offset = QPointF(v.dx(), v.dy()) * bond_offset
|
|
336
|
+
|
|
337
|
+
if is_in_ring and ring_center:
|
|
338
|
+
# 環構造: 1本の中心線(単結合位置) + 1本の短い内側線
|
|
339
|
+
# 結合の中心から環の中心への方向を計算
|
|
340
|
+
bond_center = line.center()
|
|
341
|
+
|
|
342
|
+
# ローカル座標系での環中心方向
|
|
343
|
+
local_ring_center = self.mapFromScene(ring_center)
|
|
344
|
+
local_bond_center = line.center()
|
|
345
|
+
inward_vec = local_ring_center - local_bond_center
|
|
346
|
+
|
|
347
|
+
# offsetとinward_vecの内積で内側を判定
|
|
348
|
+
if QPointF.dotProduct(offset, inward_vec) > 0:
|
|
349
|
+
# offsetが内側方向(2倍のオフセット)
|
|
350
|
+
inner_offset = offset * 2
|
|
351
|
+
else:
|
|
352
|
+
# -offsetが内側方向(2倍のオフセット)
|
|
353
|
+
inner_offset = -offset * 2
|
|
354
|
+
|
|
355
|
+
# 中心線を描画(単結合と同じ位置)
|
|
356
|
+
painter.drawLine(line)
|
|
357
|
+
|
|
358
|
+
# 内側の短い線を描画(80%の長さ)
|
|
359
|
+
inner_line = line.translated(inner_offset)
|
|
360
|
+
shorten_factor = 0.8
|
|
361
|
+
p1 = inner_line.p1()
|
|
362
|
+
p2 = inner_line.p2()
|
|
363
|
+
center = QPointF((p1.x() + p2.x()) / 2, (p1.y() + p2.y()) / 2)
|
|
364
|
+
shortened_p1 = center + (p1 - center) * shorten_factor
|
|
365
|
+
shortened_p2 = center + (p2 - center) * shorten_factor
|
|
366
|
+
painter.drawLine(QLineF(shortened_p1, shortened_p2))
|
|
367
|
+
else:
|
|
368
|
+
# 非環構造: 従来の2本の平行線
|
|
369
|
+
line1 = line.translated(offset)
|
|
370
|
+
line2 = line.translated(-offset)
|
|
371
|
+
painter.drawLine(line1)
|
|
372
|
+
painter.drawLine(line2)
|
|
373
|
+
|
|
374
|
+
# E/Z ラベルの描画処理
|
|
375
|
+
if self.stereo in [3, 4]:
|
|
376
|
+
painter.save() # 現在の描画設定を保存
|
|
377
|
+
|
|
378
|
+
# --- ラベルの設定 ---
|
|
379
|
+
font = QFont(FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD)
|
|
380
|
+
font.setItalic(True)
|
|
381
|
+
text_color = QColor("gray")
|
|
382
|
+
# 輪郭の色を背景色と同じにする(scene()がNoneのときは安全なフォールバックを使う)
|
|
383
|
+
outline_color = None
|
|
384
|
+
try:
|
|
385
|
+
sc = self.scene()
|
|
386
|
+
if sc is not None:
|
|
387
|
+
outline_color = sc.backgroundBrush().color()
|
|
388
|
+
except Exception:
|
|
389
|
+
outline_color = None
|
|
390
|
+
if outline_color is None:
|
|
391
|
+
# デフォルトでは白背景を想定して黒系の輪郭が見やすい
|
|
392
|
+
outline_color = QColor(255, 255, 255)
|
|
393
|
+
|
|
394
|
+
# --- 描画パスの作成 ---
|
|
395
|
+
text = "Z" if self.stereo == 3 else "E"
|
|
396
|
+
path = QPainterPath()
|
|
397
|
+
|
|
398
|
+
# テキストが正確に中央に来るように位置を計算
|
|
399
|
+
fm = QFontMetricsF(font)
|
|
400
|
+
text_rect = fm.boundingRect(text)
|
|
401
|
+
text_rect.moveCenter(line.center())
|
|
402
|
+
path.addText(text_rect.topLeft(), font, text)
|
|
403
|
+
|
|
404
|
+
# --- 輪郭の描画 ---
|
|
405
|
+
stroker = QPainterPathStroker()
|
|
406
|
+
stroker.setWidth(EZ_LABEL_TEXT_OUTLINE) # 輪郭の太さ
|
|
407
|
+
outline_path = stroker.createStroke(path)
|
|
408
|
+
|
|
409
|
+
painter.setBrush(outline_color)
|
|
410
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
411
|
+
painter.drawPath(outline_path)
|
|
412
|
+
|
|
413
|
+
# --- 文字本体の描画 ---
|
|
414
|
+
painter.setBrush(text_color)
|
|
415
|
+
painter.setPen(text_color)
|
|
416
|
+
painter.drawPath(path)
|
|
417
|
+
|
|
418
|
+
painter.restore() # 描画設定を元に戻す
|
|
419
|
+
|
|
420
|
+
elif self.order == 3:
|
|
421
|
+
painter.drawLine(line)
|
|
422
|
+
painter.drawLine(line.translated(offset))
|
|
423
|
+
painter.drawLine(line.translated(-offset))
|
|
424
|
+
|
|
425
|
+
# --- 2. ホバー時のエフェクトを上から重ねて描画 ---
|
|
426
|
+
if (not self.isSelected()) and getattr(self, 'hovered', False):
|
|
427
|
+
try:
|
|
428
|
+
# ホバー時のハイライトを太めの半透明な線で描画
|
|
429
|
+
hover_pen = QPen(QColor(144, 238, 144, 180), HOVER_PEN_WIDTH) # LightGreen, 半透明
|
|
430
|
+
hover_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
431
|
+
painter.setPen(hover_pen)
|
|
432
|
+
painter.drawLine(line)
|
|
433
|
+
except Exception:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def update_position(self):
|
|
439
|
+
try:
|
|
440
|
+
self.prepareGeometryChange()
|
|
441
|
+
if self.atom1:
|
|
442
|
+
self.setPos(self.atom1.pos())
|
|
443
|
+
self.update()
|
|
444
|
+
except Exception as e:
|
|
445
|
+
print(f"Error updating bond position: {e}")
|
|
446
|
+
# Continue without crashing
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def hoverEnterEvent(self, event):
|
|
450
|
+
scene = self.scene()
|
|
451
|
+
mode = getattr(scene, 'mode', '')
|
|
452
|
+
self.hovered = True
|
|
453
|
+
self.update()
|
|
454
|
+
if self.scene():
|
|
455
|
+
self.scene().set_hovered_item(self)
|
|
456
|
+
super().hoverEnterEvent(event)
|
|
457
|
+
|
|
458
|
+
def hoverLeaveEvent(self, event):
|
|
459
|
+
if self.hovered:
|
|
460
|
+
self.hovered = False
|
|
461
|
+
self.update()
|
|
462
|
+
if self.scene():
|
|
463
|
+
self.scene().set_hovered_item(None)
|
|
464
|
+
super().hoverLeaveEvent(event)
|