MoleditPy 2.3.3__tar.gz → 2.4.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-2.3.3 → moleditpy-2.4.1}/PKG-INFO +1 -1
- {moleditpy-2.3.3 → moleditpy-2.4.1}/pyproject.toml +1 -1
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/atom_item.py +61 -14
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/bond_item.py +78 -20
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/constants.py +1 -1
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window.py +8 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_edit_actions.py +105 -2
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_export.py +119 -8
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_main_init.py +38 -3
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/settings_dialog.py +361 -7
- {moleditpy-2.3.3 → moleditpy-2.4.1}/LICENSE +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/README.md +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/setup.cfg +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/__init__.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/__main__.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/main.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/__init__.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/about_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/align_plane_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/alignment_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/analysis_window.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/angle_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/assets/file_icon.ico +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/assets/icon.icns +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/assets/icon.ico +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/assets/icon.png +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/bond_length_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/calculation_worker.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/color_settings_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/custom_interactor_style.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/dihedral_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_app_state.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_compute.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_project_io.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_string_importers.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_ui_manager.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_view_3d.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/mirror_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/molecular_data.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/molecule_scene.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/move_group_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/planarize_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/plugin_interface.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/plugin_manager.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/plugin_manager_window.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/template_preview_item.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/template_preview_view.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/translation_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/user_template_dialog.py +0 -0
- {moleditpy-2.3.3 → moleditpy-2.4.1}/src/moleditpy/modules/zoomable_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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
|
|
@@ -13,7 +13,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
13
13
|
from PyQt6.QtWidgets import QGraphicsItem
|
|
14
14
|
|
|
15
15
|
from PyQt6.QtGui import (
|
|
16
|
-
QPen, QBrush, QColor, QFont, QPainterPath, QFontMetricsF
|
|
16
|
+
QPen, QBrush, QColor, QFont, QPainterPath, QFontMetricsF, QPainter
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
from PyQt6.QtCore import (
|
|
@@ -45,14 +45,41 @@ class AtomItem(QGraphicsItem):
|
|
|
45
45
|
self.setPos(pos)
|
|
46
46
|
self.implicit_h_count = 0
|
|
47
47
|
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
|
48
|
-
self.setZValue(1)
|
|
48
|
+
self.setZValue(1)
|
|
49
|
+
self.update_style()
|
|
49
50
|
self.setAcceptHoverEvents(True)
|
|
50
51
|
self.hovered = False
|
|
51
52
|
self.has_problem = False
|
|
52
53
|
|
|
54
|
+
|
|
55
|
+
def update_style(self):
|
|
56
|
+
# Allow updating font preference dynamically
|
|
57
|
+
font_size = 20
|
|
58
|
+
try:
|
|
59
|
+
if self.scene() and self.scene().views():
|
|
60
|
+
win = self.scene().views()[0].window()
|
|
61
|
+
if win and hasattr(win, 'settings'):
|
|
62
|
+
font_size = win.settings.get('atom_font_size_2d', 20)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
self.font = QFont(FONT_FAMILY, font_size, FONT_WEIGHT_BOLD)
|
|
66
|
+
self.prepareGeometryChange()
|
|
67
|
+
|
|
68
|
+
self.is_visible = not (self.symbol == 'C' and len(self.bonds) > 0 and self.charge == 0 and self.radical == 0)
|
|
69
|
+
self.update()
|
|
70
|
+
|
|
53
71
|
def boundingRect(self):
|
|
54
72
|
# --- paint()メソッドと完全に同じロジックでテキストの位置とサイズを計算 ---
|
|
55
|
-
|
|
73
|
+
# Get dynamic font size
|
|
74
|
+
font_size = 20
|
|
75
|
+
try:
|
|
76
|
+
if self.scene() and self.scene().views():
|
|
77
|
+
win = self.scene().views()[0].window()
|
|
78
|
+
if win and hasattr(win, 'settings'):
|
|
79
|
+
font_size = win.settings.get('atom_font_size_2d', 20)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
font = QFont(FONT_FAMILY, font_size, FONT_WEIGHT_BOLD)
|
|
56
83
|
fm = QFontMetricsF(font)
|
|
57
84
|
|
|
58
85
|
hydrogen_part = ""
|
|
@@ -164,9 +191,21 @@ class AtomItem(QGraphicsItem):
|
|
|
164
191
|
return path
|
|
165
192
|
|
|
166
193
|
def paint(self, painter, option, widget):
|
|
194
|
+
# Color logic: check if we should use bond color (uniform) or CPK (element-specific)
|
|
167
195
|
color = CPK_COLORS.get(self.symbol, CPK_COLORS['DEFAULT'])
|
|
196
|
+
try:
|
|
197
|
+
if self.scene() and self.scene().views():
|
|
198
|
+
win = self.scene().views()[0].window()
|
|
199
|
+
if win and hasattr(win, 'settings'):
|
|
200
|
+
if win.settings.get('atom_use_bond_color_2d', False):
|
|
201
|
+
bond_col = win.settings.get('bond_color_2d', '#222222')
|
|
202
|
+
color = QColor(bond_col)
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
|
|
168
206
|
if self.is_visible:
|
|
169
207
|
# 1. 描画の準備
|
|
208
|
+
# Ensure correct font is used (self.font should be updated by update_style)
|
|
170
209
|
painter.setFont(self.font)
|
|
171
210
|
fm = painter.fontMetrics()
|
|
172
211
|
|
|
@@ -253,19 +292,29 @@ class AtomItem(QGraphicsItem):
|
|
|
253
292
|
offset_x = -symbol_rect.width() // 2
|
|
254
293
|
text_rect.moveTo(offset_x, -text_rect.height() // 2)
|
|
255
294
|
|
|
256
|
-
# 2.
|
|
295
|
+
# 2. 原子記号の背景を処理(白で塗りつぶす か 透明なら切り抜く)
|
|
257
296
|
if self.scene():
|
|
258
297
|
bg_brush = self.scene().backgroundBrush()
|
|
259
298
|
bg_rect = text_rect.adjusted(-5, -8, 5, 8)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
299
|
+
|
|
300
|
+
if bg_brush.style() == Qt.BrushStyle.NoBrush:
|
|
301
|
+
# 背景が透明の場合は、CompositionMode_Clearを使って
|
|
302
|
+
# 重なっている結合の線を「消しゴム」のように消す
|
|
303
|
+
painter.save()
|
|
304
|
+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
|
|
305
|
+
painter.setBrush(QColor(0, 0, 0, 255)) # 色は何でも良い(アルファが重要)
|
|
306
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
307
|
+
painter.drawEllipse(bg_rect)
|
|
308
|
+
painter.restore()
|
|
309
|
+
else:
|
|
310
|
+
# 背景がある場合は、その背景色で塗りつぶす(従来通り)
|
|
311
|
+
painter.setBrush(bg_brush)
|
|
312
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
313
|
+
painter.drawEllipse(bg_rect)
|
|
263
314
|
|
|
264
315
|
# 3. 原子記号自体を描画
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
else:
|
|
268
|
-
painter.setPen(QPen(color))
|
|
316
|
+
# Color is already determined above
|
|
317
|
+
painter.setPen(QPen(color))
|
|
269
318
|
painter.drawText(text_rect, int(alignment_flag), display_text)
|
|
270
319
|
|
|
271
320
|
# --- 電荷とラジカルの描画 ---
|
|
@@ -316,9 +365,7 @@ class AtomItem(QGraphicsItem):
|
|
|
316
365
|
painter.setPen(pen)
|
|
317
366
|
painter.drawRect(self.boundingRect())
|
|
318
367
|
|
|
319
|
-
|
|
320
|
-
self.is_visible = not (self.symbol == 'C' and len(self.bonds) > 0 and self.charge == 0 and self.radical == 0)
|
|
321
|
-
self.update()
|
|
368
|
+
|
|
322
369
|
|
|
323
370
|
|
|
324
371
|
# 約203行目 AtomItem クラス内
|
|
@@ -105,7 +105,21 @@ class BondItem(QGraphicsItem):
|
|
|
105
105
|
line = self.get_line_in_local_coords()
|
|
106
106
|
except Exception:
|
|
107
107
|
line = QLineF(0, 0, 0, 0)
|
|
108
|
-
|
|
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
|
+
|
|
109
123
|
extra = (getattr(self, 'order', 1) - 1) * bond_offset + 20
|
|
110
124
|
rect = QRectF(line.p1(), line.p2()).normalized().adjusted(-extra, -extra, extra, extra)
|
|
111
125
|
|
|
@@ -142,7 +156,18 @@ class BondItem(QGraphicsItem):
|
|
|
142
156
|
view = scene.views()[0]
|
|
143
157
|
scale = view.transform().m11()
|
|
144
158
|
|
|
145
|
-
|
|
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
|
+
|
|
146
171
|
|
|
147
172
|
stroker = QPainterPathStroker()
|
|
148
173
|
stroker.setWidth(scene_width)
|
|
@@ -160,24 +185,44 @@ class BondItem(QGraphicsItem):
|
|
|
160
185
|
line = self.get_line_in_local_coords()
|
|
161
186
|
if line.length() == 0: return
|
|
162
187
|
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
176
212
|
else:
|
|
177
|
-
|
|
178
|
-
|
|
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:
|
|
179
221
|
painter.setPen(self.pen)
|
|
180
|
-
|
|
222
|
+
except Exception:
|
|
223
|
+
painter.setPen(self.pen)
|
|
224
|
+
|
|
225
|
+
painter.setBrush(QBrush(Qt.GlobalColor.black))
|
|
181
226
|
|
|
182
227
|
# --- 立体化学 (Wedge/Dash) の描画 ---
|
|
183
228
|
if self.order == 1 and self.stereo in [1, 2]:
|
|
@@ -213,7 +258,19 @@ class BondItem(QGraphicsItem):
|
|
|
213
258
|
painter.drawLine(line)
|
|
214
259
|
else:
|
|
215
260
|
v = line.unitVector().normalVector()
|
|
216
|
-
|
|
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
|
|
217
274
|
|
|
218
275
|
if self.order == 2:
|
|
219
276
|
# 環構造かどうかを判定し、描画方法を変更
|
|
@@ -274,7 +331,8 @@ class BondItem(QGraphicsItem):
|
|
|
274
331
|
is_in_ring = False
|
|
275
332
|
|
|
276
333
|
v = line.unitVector().normalVector()
|
|
277
|
-
offset
|
|
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
|
|
278
336
|
|
|
279
337
|
if is_in_ring and ring_center:
|
|
280
338
|
# 環構造: 1本の中心線(単結合位置) + 1本の短い内側線
|
|
@@ -500,6 +500,10 @@ class MainWindow(QMainWindow):
|
|
|
500
500
|
# --- MOVED TO main_window_export.py ---
|
|
501
501
|
return self.main_window_export.export_2d_png()
|
|
502
502
|
|
|
503
|
+
def export_2d_svg(self):
|
|
504
|
+
# --- MOVED TO main_window_export.py ---
|
|
505
|
+
return self.main_window_export.export_2d_svg()
|
|
506
|
+
|
|
503
507
|
def export_3d_png(self):
|
|
504
508
|
# --- MOVED TO main_window_export.py ---
|
|
505
509
|
return self.main_window_export.export_3d_png()
|
|
@@ -524,6 +528,10 @@ class MainWindow(QMainWindow):
|
|
|
524
528
|
# --- MOVED TO main_window_edit_actions.py ---
|
|
525
529
|
return self.main_window_edit_actions.adjust_molecule_positions_to_avoid_collisions(mol, frags)
|
|
526
530
|
|
|
531
|
+
def open_rotate_2d_dialog(self):
|
|
532
|
+
# --- MOVED TO main_window_edit_actions.py ---
|
|
533
|
+
return self.main_window_edit_actions.open_rotate_2d_dialog()
|
|
534
|
+
|
|
527
535
|
def draw_molecule_3d(self, mol):
|
|
528
536
|
# --- MOVED TO main_window_view_3d.py ---
|
|
529
537
|
return self.main_window_view_3d.draw_molecule_3d(mol)
|
|
@@ -34,7 +34,7 @@ from rdkit.Chem import AllChem
|
|
|
34
34
|
|
|
35
35
|
# PyQt6 Modules
|
|
36
36
|
from PyQt6.QtWidgets import (
|
|
37
|
-
QApplication
|
|
37
|
+
QApplication, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QSlider, QPushButton
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
from PyQt6.QtGui import (
|
|
@@ -43,9 +43,51 @@ from PyQt6.QtGui import (
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
from PyQt6.QtCore import (
|
|
46
|
-
QPointF, QLineF, QMimeData, QByteArray, QTimer
|
|
46
|
+
QPointF, QLineF, QMimeData, QByteArray, QTimer, Qt
|
|
47
47
|
)
|
|
48
48
|
|
|
49
|
+
class Rotate2DDialog(QDialog):
|
|
50
|
+
def __init__(self, parent=None):
|
|
51
|
+
super().__init__(parent)
|
|
52
|
+
self.setWindowTitle("Rotate 2D")
|
|
53
|
+
self.setFixedWidth(300)
|
|
54
|
+
|
|
55
|
+
layout = QVBoxLayout(self)
|
|
56
|
+
|
|
57
|
+
# Angle input
|
|
58
|
+
input_layout = QHBoxLayout()
|
|
59
|
+
input_layout.addWidget(QLabel("Angle (degrees):"))
|
|
60
|
+
self.angle_spin = QSpinBox()
|
|
61
|
+
self.angle_spin.setRange(-360, 360)
|
|
62
|
+
self.angle_spin.setValue(45)
|
|
63
|
+
input_layout.addWidget(self.angle_spin)
|
|
64
|
+
layout.addLayout(input_layout)
|
|
65
|
+
|
|
66
|
+
# Slider
|
|
67
|
+
self.slider = QSlider(Qt.Orientation.Horizontal)
|
|
68
|
+
self.slider.setRange(-180, 180)
|
|
69
|
+
self.slider.setValue(45)
|
|
70
|
+
self.slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
71
|
+
self.slider.setTickInterval(15)
|
|
72
|
+
layout.addWidget(self.slider)
|
|
73
|
+
|
|
74
|
+
# Sync slider and spinbox
|
|
75
|
+
self.angle_spin.valueChanged.connect(self.slider.setValue)
|
|
76
|
+
self.slider.valueChanged.connect(self.angle_spin.setValue)
|
|
77
|
+
|
|
78
|
+
# Buttons
|
|
79
|
+
btn_layout = QHBoxLayout()
|
|
80
|
+
ok_btn = QPushButton("Rotate")
|
|
81
|
+
ok_btn.clicked.connect(self.accept)
|
|
82
|
+
cancel_btn = QPushButton("Cancel")
|
|
83
|
+
cancel_btn.clicked.connect(self.reject)
|
|
84
|
+
btn_layout.addWidget(ok_btn)
|
|
85
|
+
btn_layout.addWidget(cancel_btn)
|
|
86
|
+
layout.addLayout(btn_layout)
|
|
87
|
+
|
|
88
|
+
def get_angle(self):
|
|
89
|
+
return self.angle_spin.value()
|
|
90
|
+
|
|
49
91
|
|
|
50
92
|
# Use centralized Open Babel availability from package-level __init__
|
|
51
93
|
# Use per-package modules availability (local __init__).
|
|
@@ -568,6 +610,67 @@ class MainWindowEditActions(object):
|
|
|
568
610
|
|
|
569
611
|
|
|
570
612
|
|
|
613
|
+
def open_rotate_2d_dialog(self):
|
|
614
|
+
"""2D回転ダイアログを開く"""
|
|
615
|
+
dialog = Rotate2DDialog(self)
|
|
616
|
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
617
|
+
angle = dialog.get_angle()
|
|
618
|
+
self.rotate_molecule_2d(angle)
|
|
619
|
+
|
|
620
|
+
def rotate_molecule_2d(self, angle_degrees):
|
|
621
|
+
"""2D分子を指定角度回転させる(選択範囲があればそれのみ、なければ全体)"""
|
|
622
|
+
try:
|
|
623
|
+
# Determine target atoms
|
|
624
|
+
selected_items = self.scene.selectedItems()
|
|
625
|
+
target_atoms = [item for item in selected_items if isinstance(item, AtomItem)]
|
|
626
|
+
|
|
627
|
+
# If no selection, rotate everything
|
|
628
|
+
if not target_atoms:
|
|
629
|
+
target_atoms = [data['item'] for data in self.data.atoms.values() if data.get('item') and not sip_isdeleted_safe(data['item'])]
|
|
630
|
+
|
|
631
|
+
if not target_atoms:
|
|
632
|
+
self.statusBar().showMessage("No atoms to rotate.")
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
# Calculate Center
|
|
636
|
+
xs = [atom.pos().x() for atom in target_atoms]
|
|
637
|
+
ys = [atom.pos().y() for atom in target_atoms]
|
|
638
|
+
if not xs: return
|
|
639
|
+
|
|
640
|
+
center_x = sum(xs) / len(xs)
|
|
641
|
+
center_y = sum(ys) / len(ys)
|
|
642
|
+
center = QPointF(center_x, center_y)
|
|
643
|
+
|
|
644
|
+
rad = math.radians(angle_degrees)
|
|
645
|
+
cos_a = math.cos(rad)
|
|
646
|
+
sin_a = math.sin(rad)
|
|
647
|
+
|
|
648
|
+
for atom in target_atoms:
|
|
649
|
+
# Relative pos
|
|
650
|
+
dx = atom.pos().x() - center_x
|
|
651
|
+
dy = atom.pos().y() - center_y
|
|
652
|
+
|
|
653
|
+
# Rotate
|
|
654
|
+
new_dx = dx * cos_a - dy * sin_a
|
|
655
|
+
new_dy = dx * sin_a + dy * cos_a
|
|
656
|
+
|
|
657
|
+
new_pos = QPointF(center_x + new_dx, center_y + new_dy)
|
|
658
|
+
atom.setPos(new_pos)
|
|
659
|
+
|
|
660
|
+
# Update bonds
|
|
661
|
+
self.scene.update_connected_bonds(target_atoms)
|
|
662
|
+
|
|
663
|
+
self.push_undo_state()
|
|
664
|
+
self.statusBar().showMessage(f"Rotated {len(target_atoms)} atoms by {angle_degrees} degrees.")
|
|
665
|
+
self.scene.update()
|
|
666
|
+
|
|
667
|
+
except Exception as e:
|
|
668
|
+
print(f"Error rotating molecule: {e}")
|
|
669
|
+
traceback.print_exc()
|
|
670
|
+
self.statusBar().showMessage(f"Error rotating: {e}")
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
|
|
571
674
|
|
|
572
675
|
def select_all(self):
|
|
573
676
|
for item in self.scene.items():
|
|
@@ -37,9 +37,11 @@ from PyQt6.QtGui import (
|
|
|
37
37
|
QBrush, QColor, QPainter, QImage
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
+
from PyQt6.QtSvg import QSvgGenerator
|
|
41
|
+
|
|
40
42
|
|
|
41
43
|
from PyQt6.QtCore import (
|
|
42
|
-
Qt, QRectF
|
|
44
|
+
Qt, QRectF, QSize
|
|
43
45
|
)
|
|
44
46
|
|
|
45
47
|
import pyvista as pv
|
|
@@ -672,7 +674,7 @@ class MainWindowExport(object):
|
|
|
672
674
|
filePath += ".png"
|
|
673
675
|
|
|
674
676
|
reply = QMessageBox.question(self, 'Choose Background',
|
|
675
|
-
'Do you want a transparent background?\n(Choose "No"
|
|
677
|
+
'Do you want a transparent background?\n(Choose "No" to use the current background color)',
|
|
676
678
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel,
|
|
677
679
|
QMessageBox.StandardButton.Yes)
|
|
678
680
|
|
|
@@ -706,8 +708,7 @@ class MainWindowExport(object):
|
|
|
706
708
|
|
|
707
709
|
if is_transparent:
|
|
708
710
|
self.scene.setBackgroundBrush(QBrush(Qt.BrushStyle.NoBrush))
|
|
709
|
-
|
|
710
|
-
self.scene.setBackgroundBrush(QBrush(QColor("#FFFFFF")))
|
|
711
|
+
# Else: keep original_background (current 2D background)
|
|
711
712
|
|
|
712
713
|
rect_to_render = molecule_bounds.adjusted(-20, -20, 20, 20)
|
|
713
714
|
|
|
@@ -719,10 +720,8 @@ class MainWindowExport(object):
|
|
|
719
720
|
return
|
|
720
721
|
|
|
721
722
|
image = QImage(w, h, QImage.Format.Format_ARGB32_Premultiplied)
|
|
722
|
-
if
|
|
723
|
-
|
|
724
|
-
else:
|
|
725
|
-
image.fill(Qt.GlobalColor.white)
|
|
723
|
+
# Always fill with transparent; render will paint opaque background if present
|
|
724
|
+
image.fill(Qt.GlobalColor.transparent)
|
|
726
725
|
|
|
727
726
|
painter = QPainter()
|
|
728
727
|
ok = painter.begin(image)
|
|
@@ -756,6 +755,118 @@ class MainWindowExport(object):
|
|
|
756
755
|
|
|
757
756
|
|
|
758
757
|
|
|
758
|
+
def export_2d_svg(self):
|
|
759
|
+
"""2D drawingをSVGとしてエクスポート"""
|
|
760
|
+
if not self.data.atoms:
|
|
761
|
+
self.statusBar().showMessage("Nothing to export.")
|
|
762
|
+
return
|
|
763
|
+
|
|
764
|
+
# default filename
|
|
765
|
+
default_name = "untitled-2d"
|
|
766
|
+
try:
|
|
767
|
+
if self.current_file_path:
|
|
768
|
+
base = os.path.basename(self.current_file_path)
|
|
769
|
+
name = os.path.splitext(base)[0]
|
|
770
|
+
default_name = f"{name}-2d"
|
|
771
|
+
except Exception:
|
|
772
|
+
default_name = "untitled-2d"
|
|
773
|
+
|
|
774
|
+
# prefer same directory
|
|
775
|
+
default_path = default_name
|
|
776
|
+
try:
|
|
777
|
+
if self.current_file_path:
|
|
778
|
+
default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
|
|
779
|
+
except Exception:
|
|
780
|
+
default_path = default_name
|
|
781
|
+
|
|
782
|
+
filePath, _ = QFileDialog.getSaveFileName(self, "Export 2D as SVG", default_path, "SVG Files (*.svg)")
|
|
783
|
+
if not filePath:
|
|
784
|
+
return
|
|
785
|
+
|
|
786
|
+
if not (filePath.lower().endswith(".svg")):
|
|
787
|
+
filePath += ".svg"
|
|
788
|
+
|
|
789
|
+
# Ask about transparency
|
|
790
|
+
reply = QMessageBox.question(self, 'Choose Background',
|
|
791
|
+
'Do you want a transparent background?\n(Choose "No" to use the current background color)',
|
|
792
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel,
|
|
793
|
+
QMessageBox.StandardButton.Yes)
|
|
794
|
+
|
|
795
|
+
if reply == QMessageBox.StandardButton.Cancel:
|
|
796
|
+
self.statusBar().showMessage("Export cancelled.", 2000)
|
|
797
|
+
return
|
|
798
|
+
|
|
799
|
+
is_transparent = (reply == QMessageBox.StandardButton.Yes)
|
|
800
|
+
|
|
801
|
+
try:
|
|
802
|
+
# 1. Hide non-molecular items if needed (optional, keeping consistent with PNG export)
|
|
803
|
+
items_to_restore = {}
|
|
804
|
+
original_background = self.scene.backgroundBrush()
|
|
805
|
+
|
|
806
|
+
all_items = list(self.scene.items())
|
|
807
|
+
for item in all_items:
|
|
808
|
+
is_mol_part = isinstance(item, (AtomItem, BondItem))
|
|
809
|
+
if not (is_mol_part and item.isVisible()):
|
|
810
|
+
# Keep measurement items visible if they are part of the scene?
|
|
811
|
+
# For now, let's stick to hiding everything that isn't atom/bond,
|
|
812
|
+
# similar to png export logic, or we can decide to export everything visible.
|
|
813
|
+
# The PNG export hides non-atom/bond items. Let's follow that for consistency.
|
|
814
|
+
items_to_restore[item] = item.isVisible()
|
|
815
|
+
item.hide()
|
|
816
|
+
|
|
817
|
+
# 2. Calculate bounds
|
|
818
|
+
molecule_bounds = QRectF()
|
|
819
|
+
for item in self.scene.items():
|
|
820
|
+
if isinstance(item, (AtomItem, BondItem)) and item.isVisible():
|
|
821
|
+
molecule_bounds = molecule_bounds.united(item.sceneBoundingRect())
|
|
822
|
+
|
|
823
|
+
if molecule_bounds.isEmpty() or not molecule_bounds.isValid():
|
|
824
|
+
self.statusBar().showMessage("Error: Could not determine molecule bounds for export.")
|
|
825
|
+
# Restore
|
|
826
|
+
for item, was_visible in items_to_restore.items():
|
|
827
|
+
item.setVisible(was_visible)
|
|
828
|
+
return
|
|
829
|
+
|
|
830
|
+
if is_transparent:
|
|
831
|
+
self.scene.setBackgroundBrush(QBrush(Qt.BrushStyle.NoBrush))
|
|
832
|
+
|
|
833
|
+
# Margin
|
|
834
|
+
rect_to_render = molecule_bounds.adjusted(-20, -20, 20, 20)
|
|
835
|
+
|
|
836
|
+
width = int(rect_to_render.width())
|
|
837
|
+
height = int(rect_to_render.height())
|
|
838
|
+
|
|
839
|
+
# 3. Setup QSvgGenerator
|
|
840
|
+
generator = QSvgGenerator()
|
|
841
|
+
generator.setFileName(filePath)
|
|
842
|
+
generator.setSize(QSize(width, height))
|
|
843
|
+
generator.setViewBox(rect_to_render)
|
|
844
|
+
generator.setTitle("MoleditPy Molecule")
|
|
845
|
+
|
|
846
|
+
# 4. Render
|
|
847
|
+
painter = QPainter()
|
|
848
|
+
painter.begin(generator)
|
|
849
|
+
try:
|
|
850
|
+
self.scene.render(painter, rect_to_render, rect_to_render)
|
|
851
|
+
finally:
|
|
852
|
+
painter.end()
|
|
853
|
+
|
|
854
|
+
self.statusBar().showMessage(f"2D view exported to {filePath}")
|
|
855
|
+
|
|
856
|
+
except Exception as e:
|
|
857
|
+
self.statusBar().showMessage(f"An unexpected error occurred during SVG export: {e}")
|
|
858
|
+
|
|
859
|
+
finally:
|
|
860
|
+
# Restore
|
|
861
|
+
for item, was_visible in items_to_restore.items():
|
|
862
|
+
item.setVisible(was_visible)
|
|
863
|
+
if 'original_background' in locals():
|
|
864
|
+
self.scene.setBackgroundBrush(original_background)
|
|
865
|
+
if self.view_2d:
|
|
866
|
+
self.view_2d.viewport().update()
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
|
|
759
870
|
def export_3d_png(self):
|
|
760
871
|
if not self.current_mol:
|
|
761
872
|
self.statusBar().showMessage("No 3D molecule to export.", 2000)
|