MoleditPy 1.15.1__py3-none-any.whl → 1.16.0a1__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/__init__.py +4 -0
- moleditpy/__main__.py +29 -19748
- moleditpy/main.py +40 -0
- moleditpy/modules/__init__.py +36 -0
- moleditpy/modules/about_dialog.py +92 -0
- moleditpy/modules/align_plane_dialog.py +281 -0
- moleditpy/modules/alignment_dialog.py +261 -0
- moleditpy/modules/analysis_window.py +197 -0
- moleditpy/modules/angle_dialog.py +428 -0
- moleditpy/modules/assets/icon.icns +0 -0
- moleditpy/modules/atom_item.py +336 -0
- moleditpy/modules/bond_item.py +303 -0
- moleditpy/modules/bond_length_dialog.py +368 -0
- moleditpy/modules/calculation_worker.py +754 -0
- moleditpy/modules/color_settings_dialog.py +309 -0
- moleditpy/modules/constants.py +76 -0
- moleditpy/modules/constrained_optimization_dialog.py +667 -0
- moleditpy/modules/custom_interactor_style.py +737 -0
- moleditpy/modules/custom_qt_interactor.py +49 -0
- moleditpy/modules/dialog3_d_picking_mixin.py +96 -0
- moleditpy/modules/dihedral_dialog.py +431 -0
- moleditpy/modules/main_window.py +827 -0
- moleditpy/modules/main_window_app_state.py +709 -0
- moleditpy/modules/main_window_compute.py +1203 -0
- moleditpy/modules/main_window_dialog_manager.py +454 -0
- moleditpy/modules/main_window_edit_3d.py +531 -0
- moleditpy/modules/main_window_edit_actions.py +1449 -0
- moleditpy/modules/main_window_export.py +744 -0
- moleditpy/modules/main_window_main_init.py +1560 -0
- moleditpy/modules/main_window_molecular_parsers.py +956 -0
- moleditpy/modules/main_window_project_io.py +416 -0
- moleditpy/modules/main_window_string_importers.py +270 -0
- moleditpy/modules/main_window_ui_manager.py +567 -0
- moleditpy/modules/main_window_view_3d.py +1163 -0
- moleditpy/modules/main_window_view_loaders.py +350 -0
- moleditpy/modules/mirror_dialog.py +110 -0
- moleditpy/modules/molecular_data.py +290 -0
- moleditpy/modules/molecule_scene.py +1895 -0
- moleditpy/modules/move_group_dialog.py +586 -0
- moleditpy/modules/periodic_table_dialog.py +72 -0
- moleditpy/modules/planarize_dialog.py +209 -0
- moleditpy/modules/settings_dialog.py +1034 -0
- moleditpy/modules/template_preview_item.py +148 -0
- moleditpy/modules/template_preview_view.py +62 -0
- moleditpy/modules/translation_dialog.py +353 -0
- moleditpy/modules/user_template_dialog.py +621 -0
- moleditpy/modules/zoomable_view.py +98 -0
- {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/METADATA +1 -1
- moleditpy-1.16.0a1.dist-info/RECORD +54 -0
- moleditpy-1.15.1.dist-info/RECORD +0 -9
- /moleditpy/{assets → modules/assets}/icon.ico +0 -0
- /moleditpy/{assets → modules/assets}/icon.png +0 -0
- {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/WHEEL +0 -0
- {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/entry_points.txt +0 -0
- {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
main_window_export.py
|
|
6
|
+
MainWindow (main_window.py) から分離されたモジュール
|
|
7
|
+
機能クラス: MainWindowExport
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import math
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# RDKit imports (explicit to satisfy flake8 and used features)
|
|
17
|
+
try:
|
|
18
|
+
pass
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
# PyQt6 Modules
|
|
23
|
+
from PyQt6.QtWidgets import (
|
|
24
|
+
QApplication, QFileDialog, QMessageBox
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from PyQt6.QtGui import (
|
|
28
|
+
QBrush, QColor, QPainter, QImage
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
from PyQt6.QtCore import (
|
|
33
|
+
Qt, QRectF
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
import pyvista as pv
|
|
37
|
+
|
|
38
|
+
# Use centralized Open Babel availability from package-level __init__
|
|
39
|
+
# Use per-package modules availability (local __init__).
|
|
40
|
+
try:
|
|
41
|
+
from . import OBABEL_AVAILABLE
|
|
42
|
+
except Exception:
|
|
43
|
+
from modules import OBABEL_AVAILABLE
|
|
44
|
+
# Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
|
|
45
|
+
if OBABEL_AVAILABLE:
|
|
46
|
+
try:
|
|
47
|
+
from openbabel import pybel
|
|
48
|
+
except Exception:
|
|
49
|
+
# If import fails here, disable OBABEL locally; avoid raising
|
|
50
|
+
pybel = None
|
|
51
|
+
OBABEL_AVAILABLE = False
|
|
52
|
+
print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
|
|
53
|
+
else:
|
|
54
|
+
pybel = None
|
|
55
|
+
|
|
56
|
+
# Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
|
|
57
|
+
# allows safely detecting C++ wrapper objects that have been deleted. Import
|
|
58
|
+
# it once at module import time and expose a small, robust wrapper so callers
|
|
59
|
+
# can avoid re-importing sip repeatedly and so we centralize exception
|
|
60
|
+
# handling (this reduces crash risk during teardown and deletion operations).
|
|
61
|
+
try:
|
|
62
|
+
import sip as _sip # type: ignore
|
|
63
|
+
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
64
|
+
except Exception:
|
|
65
|
+
_sip = None
|
|
66
|
+
_sip_isdeleted = None
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# package relative imports (preferred when running as `python -m moleditpy`)
|
|
70
|
+
from .atom_item import AtomItem
|
|
71
|
+
from .bond_item import BondItem
|
|
72
|
+
except Exception:
|
|
73
|
+
# Fallback to absolute imports for script-style execution
|
|
74
|
+
from modules.atom_item import AtomItem
|
|
75
|
+
from modules.bond_item import BondItem
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# --- クラス定義 ---
|
|
79
|
+
class MainWindowExport(object):
|
|
80
|
+
""" main_window.py から分離された機能クラス """
|
|
81
|
+
|
|
82
|
+
def __init__(self, main_window):
|
|
83
|
+
""" クラスの初期化 """
|
|
84
|
+
self.mw = main_window
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def export_stl(self):
|
|
88
|
+
"""STLファイルとしてエクスポート(色なし)"""
|
|
89
|
+
if not self.current_mol:
|
|
90
|
+
self.statusBar().showMessage("Error: Please generate a 3D structure first.")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# prefer same directory as current file when available
|
|
94
|
+
default_dir = ""
|
|
95
|
+
try:
|
|
96
|
+
if self.current_file_path:
|
|
97
|
+
default_dir = os.path.dirname(self.current_file_path)
|
|
98
|
+
except Exception:
|
|
99
|
+
default_dir = ""
|
|
100
|
+
|
|
101
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
102
|
+
self, "Export as STL", default_dir, "STL Files (*.stl);;All Files (*)"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not file_path:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
|
|
110
|
+
# 3Dビューから直接データを取得(色情報なし)
|
|
111
|
+
combined_mesh = self.export_from_3d_view_no_color()
|
|
112
|
+
|
|
113
|
+
if combined_mesh is None or combined_mesh.n_points == 0:
|
|
114
|
+
self.statusBar().showMessage("No 3D geometry to export.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if not file_path.lower().endswith('.stl'):
|
|
118
|
+
file_path += '.stl'
|
|
119
|
+
|
|
120
|
+
combined_mesh.save(file_path, binary=True)
|
|
121
|
+
self.statusBar().showMessage(f"STL exported to {file_path}")
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
self.statusBar().showMessage(f"Error exporting STL: {e}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def export_obj_mtl(self):
|
|
129
|
+
"""OBJ/MTLファイルとしてエクスポート(表示中のモデルベース、色付き)"""
|
|
130
|
+
if not self.current_mol:
|
|
131
|
+
self.statusBar().showMessage("Error: Please generate a 3D structure first.")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
# prefer same directory as current file when available
|
|
135
|
+
default_dir = ""
|
|
136
|
+
try:
|
|
137
|
+
if self.current_file_path:
|
|
138
|
+
default_dir = os.path.dirname(self.current_file_path)
|
|
139
|
+
except Exception:
|
|
140
|
+
default_dir = ""
|
|
141
|
+
|
|
142
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
143
|
+
self, "Export as OBJ/MTL (with colors)", default_dir, "OBJ Files (*.obj);;All Files (*)"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if not file_path:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
|
|
151
|
+
# 3Dビューから表示中のメッシュデータを色情報とともに取得
|
|
152
|
+
meshes_with_colors = self.export_from_3d_view_with_colors()
|
|
153
|
+
|
|
154
|
+
if not meshes_with_colors:
|
|
155
|
+
self.statusBar().showMessage("No 3D geometry to export.")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# ファイル拡張子を確認・追加
|
|
159
|
+
if not file_path.lower().endswith('.obj'):
|
|
160
|
+
file_path += '.obj'
|
|
161
|
+
|
|
162
|
+
# OBJ+MTL形式で保存(オブジェクトごとに色分け)
|
|
163
|
+
mtl_path = file_path.replace('.obj', '.mtl')
|
|
164
|
+
|
|
165
|
+
self.create_multi_material_obj(meshes_with_colors, file_path, mtl_path)
|
|
166
|
+
|
|
167
|
+
self.statusBar().showMessage(f"OBJ+MTL files with individual colors exported to {file_path} and {mtl_path}")
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
self.statusBar().showMessage(f"Error exporting OBJ/MTL: {e}")
|
|
171
|
+
|
|
172
|
+
return meshes_with_colors
|
|
173
|
+
|
|
174
|
+
except Exception:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def create_multi_material_obj(self, meshes_with_colors, obj_path, mtl_path):
|
|
180
|
+
"""複数のマテリアルを持つOBJファイルとMTLファイルを作成(改良版)"""
|
|
181
|
+
try:
|
|
182
|
+
|
|
183
|
+
# MTLファイルを作成
|
|
184
|
+
with open(mtl_path, 'w') as mtl_file:
|
|
185
|
+
mtl_file.write(f"# Material file for {os.path.basename(obj_path)}\n")
|
|
186
|
+
mtl_file.write("# Generated with individual object colors\n\n")
|
|
187
|
+
|
|
188
|
+
for i, mesh_data in enumerate(meshes_with_colors):
|
|
189
|
+
color = mesh_data['color']
|
|
190
|
+
material_name = f"material_{i}_{mesh_data['name'].replace(' ', '_')}"
|
|
191
|
+
|
|
192
|
+
mtl_file.write(f"newmtl {material_name}\n")
|
|
193
|
+
mtl_file.write("Ka 0.2 0.2 0.2\n") # Ambient
|
|
194
|
+
mtl_file.write(f"Kd {color[0]/255.0:.3f} {color[1]/255.0:.3f} {color[2]/255.0:.3f}\n") # Diffuse
|
|
195
|
+
mtl_file.write("Ks 0.5 0.5 0.5\n") # Specular
|
|
196
|
+
mtl_file.write("Ns 32.0\n") # Specular exponent
|
|
197
|
+
mtl_file.write("illum 2\n") # Illumination model
|
|
198
|
+
mtl_file.write("\n")
|
|
199
|
+
|
|
200
|
+
# OBJファイルを作成
|
|
201
|
+
with open(obj_path, 'w') as obj_file:
|
|
202
|
+
obj_file.write("# OBJ file with multiple materials\n")
|
|
203
|
+
obj_file.write("# Generated with individual object colors\n")
|
|
204
|
+
obj_file.write(f"mtllib {os.path.basename(mtl_path)}\n\n")
|
|
205
|
+
|
|
206
|
+
vertex_offset = 1 # OBJファイルの頂点インデックスは1から始まる
|
|
207
|
+
|
|
208
|
+
for i, mesh_data in enumerate(meshes_with_colors):
|
|
209
|
+
mesh = mesh_data['mesh']
|
|
210
|
+
material_name = f"material_{i}_{mesh_data['name'].replace(' ', '_')}"
|
|
211
|
+
|
|
212
|
+
obj_file.write(f"# Object {i}: {mesh_data['name']}\n")
|
|
213
|
+
obj_file.write(f"# Color: RGB({mesh_data['color'][0]}, {mesh_data['color'][1]}, {mesh_data['color'][2]})\n")
|
|
214
|
+
obj_file.write(f"o object_{i}\n")
|
|
215
|
+
obj_file.write(f"usemtl {material_name}\n")
|
|
216
|
+
|
|
217
|
+
# 頂点を書き込み
|
|
218
|
+
points = mesh.points
|
|
219
|
+
for point in points:
|
|
220
|
+
obj_file.write(f"v {point[0]:.6f} {point[1]:.6f} {point[2]:.6f}\n")
|
|
221
|
+
|
|
222
|
+
# 面を書き込み
|
|
223
|
+
for j in range(mesh.n_cells):
|
|
224
|
+
cell = mesh.get_cell(j)
|
|
225
|
+
if cell.type == 5: # VTK_TRIANGLE
|
|
226
|
+
points_in_cell = cell.point_ids
|
|
227
|
+
v1 = points_in_cell[0] + vertex_offset
|
|
228
|
+
v2 = points_in_cell[1] + vertex_offset
|
|
229
|
+
v3 = points_in_cell[2] + vertex_offset
|
|
230
|
+
obj_file.write(f"f {v1} {v2} {v3}\n")
|
|
231
|
+
elif cell.type == 9: # VTK_QUAD
|
|
232
|
+
points_in_cell = cell.point_ids
|
|
233
|
+
v1 = points_in_cell[0] + vertex_offset
|
|
234
|
+
v2 = points_in_cell[1] + vertex_offset
|
|
235
|
+
v3 = points_in_cell[2] + vertex_offset
|
|
236
|
+
v4 = points_in_cell[3] + vertex_offset
|
|
237
|
+
obj_file.write(f"f {v1} {v2} {v3} {v4}\n")
|
|
238
|
+
|
|
239
|
+
vertex_offset += mesh.n_points
|
|
240
|
+
obj_file.write("\n")
|
|
241
|
+
|
|
242
|
+
except Exception as e:
|
|
243
|
+
raise Exception(f"Failed to create multi-material OBJ: {e}")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def export_color_stl(self):
|
|
248
|
+
"""カラーSTLファイルとしてエクスポート"""
|
|
249
|
+
if not self.current_mol:
|
|
250
|
+
self.statusBar().showMessage("Error: Please generate a 3D structure first.")
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# prefer same directory as current file when available
|
|
254
|
+
default_dir = ""
|
|
255
|
+
try:
|
|
256
|
+
if self.current_file_path:
|
|
257
|
+
default_dir = os.path.dirname(self.current_file_path)
|
|
258
|
+
except Exception:
|
|
259
|
+
default_dir = ""
|
|
260
|
+
|
|
261
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
262
|
+
self, "Export as Color STL", default_dir, "STL Files (*.stl);;All Files (*)"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if not file_path:
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
|
|
270
|
+
# 3Dビューから直接データを取得
|
|
271
|
+
combined_mesh = self.export_from_3d_view()
|
|
272
|
+
|
|
273
|
+
if combined_mesh is None or combined_mesh.n_points == 0:
|
|
274
|
+
self.statusBar().showMessage("No 3D geometry to export.")
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
# STL形式で保存
|
|
278
|
+
if not file_path.lower().endswith('.stl'):
|
|
279
|
+
file_path += '.stl'
|
|
280
|
+
combined_mesh.save(file_path, binary=True)
|
|
281
|
+
self.statusBar().showMessage(f"STL exported to {file_path}")
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
self.statusBar().showMessage(f"Error exporting STL: {e}")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def export_from_3d_view(self):
|
|
289
|
+
"""現在の3Dビューから直接メッシュデータを取得"""
|
|
290
|
+
try:
|
|
291
|
+
|
|
292
|
+
# PyVistaプロッターから全てのアクターを取得
|
|
293
|
+
combined_mesh = pv.PolyData()
|
|
294
|
+
|
|
295
|
+
# プロッターのレンダラーからアクターを取得
|
|
296
|
+
renderer = self.plotter.renderer
|
|
297
|
+
actors = renderer.actors
|
|
298
|
+
|
|
299
|
+
for actor_name, actor in actors.items():
|
|
300
|
+
try:
|
|
301
|
+
# VTKアクターからポリデータを取得する複数の方法を試行
|
|
302
|
+
mesh = None
|
|
303
|
+
|
|
304
|
+
# 方法1: mapperのinputから取得
|
|
305
|
+
if hasattr(actor, 'mapper') and actor.mapper is not None:
|
|
306
|
+
if hasattr(actor.mapper, 'input') and actor.mapper.input is not None:
|
|
307
|
+
mesh = actor.mapper.input
|
|
308
|
+
elif hasattr(actor.mapper, 'GetInput') and actor.mapper.GetInput() is not None:
|
|
309
|
+
mesh = actor.mapper.GetInput()
|
|
310
|
+
|
|
311
|
+
# 方法2: PyVistaプロッターの内部データから取得
|
|
312
|
+
if mesh is None and actor_name in self.plotter.mesh:
|
|
313
|
+
mesh = self.plotter.mesh[actor_name]
|
|
314
|
+
|
|
315
|
+
# 方法3: PyVistaのメッシュデータベースから検索
|
|
316
|
+
if mesh is None:
|
|
317
|
+
for mesh_name, mesh_data in self.plotter.mesh.items():
|
|
318
|
+
if mesh_data is not None and mesh_data.n_points > 0:
|
|
319
|
+
mesh = mesh_data
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
if mesh is not None and hasattr(mesh, 'n_points') and mesh.n_points > 0:
|
|
323
|
+
# PyVistaメッシュに変換(必要な場合)
|
|
324
|
+
if not isinstance(mesh, pv.PolyData):
|
|
325
|
+
if hasattr(mesh, 'extract_surface'):
|
|
326
|
+
mesh = mesh.extract_surface()
|
|
327
|
+
else:
|
|
328
|
+
mesh = pv.wrap(mesh)
|
|
329
|
+
|
|
330
|
+
# 元のメッシュを変更しないようにコピーを作成
|
|
331
|
+
mesh_copy = mesh.copy()
|
|
332
|
+
|
|
333
|
+
# コピーしたメッシュにカラー情報を追加
|
|
334
|
+
if hasattr(actor, 'prop') and hasattr(actor.prop, 'color'):
|
|
335
|
+
color = actor.prop.color
|
|
336
|
+
# RGB値を0-255の範囲に変換
|
|
337
|
+
rgb = np.array([int(c * 255) for c in color], dtype=np.uint8)
|
|
338
|
+
|
|
339
|
+
# Blender対応のPLY形式用カラー属性を設定
|
|
340
|
+
mesh_copy.point_data['diffuse_red'] = np.full(mesh_copy.n_points, rgb[0], dtype=np.uint8)
|
|
341
|
+
mesh_copy.point_data['diffuse_green'] = np.full(mesh_copy.n_points, rgb[1], dtype=np.uint8)
|
|
342
|
+
mesh_copy.point_data['diffuse_blue'] = np.full(mesh_copy.n_points, rgb[2], dtype=np.uint8)
|
|
343
|
+
|
|
344
|
+
# 標準的なPLY形式もサポート
|
|
345
|
+
mesh_copy.point_data['red'] = np.full(mesh_copy.n_points, rgb[0], dtype=np.uint8)
|
|
346
|
+
mesh_copy.point_data['green'] = np.full(mesh_copy.n_points, rgb[1], dtype=np.uint8)
|
|
347
|
+
mesh_copy.point_data['blue'] = np.full(mesh_copy.n_points, rgb[2], dtype=np.uint8)
|
|
348
|
+
|
|
349
|
+
# 従来の colors 配列も保持(STL用)
|
|
350
|
+
mesh_colors = np.tile(rgb, (mesh_copy.n_points, 1))
|
|
351
|
+
mesh_copy.point_data['colors'] = mesh_colors
|
|
352
|
+
|
|
353
|
+
# メッシュを結合
|
|
354
|
+
if combined_mesh.n_points == 0:
|
|
355
|
+
combined_mesh = mesh_copy.copy()
|
|
356
|
+
else:
|
|
357
|
+
combined_mesh = combined_mesh.merge(mesh_copy)
|
|
358
|
+
|
|
359
|
+
except Exception:
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
return combined_mesh
|
|
363
|
+
|
|
364
|
+
except Exception:
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def export_from_3d_view_no_color(self):
|
|
370
|
+
"""現在の3Dビューから直接メッシュデータを取得(色情報なし)"""
|
|
371
|
+
try:
|
|
372
|
+
|
|
373
|
+
# PyVistaプロッターから全てのアクターを取得
|
|
374
|
+
combined_mesh = pv.PolyData()
|
|
375
|
+
|
|
376
|
+
# プロッターのレンダラーからアクターを取得
|
|
377
|
+
renderer = self.plotter.renderer
|
|
378
|
+
actors = renderer.actors
|
|
379
|
+
|
|
380
|
+
for actor_name, actor in actors.items():
|
|
381
|
+
try:
|
|
382
|
+
# VTKアクターからポリデータを取得する複数の方法を試行
|
|
383
|
+
mesh = None
|
|
384
|
+
|
|
385
|
+
# 方法1: mapperのinputから取得
|
|
386
|
+
if hasattr(actor, 'mapper') and actor.mapper is not None:
|
|
387
|
+
if hasattr(actor.mapper, 'input') and actor.mapper.input is not None:
|
|
388
|
+
mesh = actor.mapper.input
|
|
389
|
+
elif hasattr(actor.mapper, 'GetInput') and actor.mapper.GetInput() is not None:
|
|
390
|
+
mesh = actor.mapper.GetInput()
|
|
391
|
+
|
|
392
|
+
# 方法2: PyVistaプロッターの内部データから取得
|
|
393
|
+
if mesh is None and actor_name in self.plotter.mesh:
|
|
394
|
+
mesh = self.plotter.mesh[actor_name]
|
|
395
|
+
|
|
396
|
+
# 方法3: PyVistaのメッシュデータベースから検索
|
|
397
|
+
if mesh is None:
|
|
398
|
+
for mesh_name, mesh_data in self.plotter.mesh.items():
|
|
399
|
+
if mesh_data is not None and mesh_data.n_points > 0:
|
|
400
|
+
mesh = mesh_data
|
|
401
|
+
break
|
|
402
|
+
|
|
403
|
+
if mesh is not None and hasattr(mesh, 'n_points') and mesh.n_points > 0:
|
|
404
|
+
# PyVistaメッシュに変換(必要な場合)
|
|
405
|
+
if not isinstance(mesh, pv.PolyData):
|
|
406
|
+
if hasattr(mesh, 'extract_surface'):
|
|
407
|
+
mesh = mesh.extract_surface()
|
|
408
|
+
else:
|
|
409
|
+
mesh = pv.wrap(mesh)
|
|
410
|
+
|
|
411
|
+
# 元のメッシュを変更しないようにコピーを作成(色情報は追加しない)
|
|
412
|
+
mesh_copy = mesh.copy()
|
|
413
|
+
|
|
414
|
+
# メッシュを結合
|
|
415
|
+
if combined_mesh.n_points == 0:
|
|
416
|
+
combined_mesh = mesh_copy.copy()
|
|
417
|
+
else:
|
|
418
|
+
combined_mesh = combined_mesh.merge(mesh_copy)
|
|
419
|
+
|
|
420
|
+
except Exception:
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
return combined_mesh
|
|
424
|
+
|
|
425
|
+
except Exception:
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def export_from_3d_view_with_colors(self):
|
|
431
|
+
"""現在の3Dビューから直接メッシュデータを色情報とともに取得"""
|
|
432
|
+
try:
|
|
433
|
+
|
|
434
|
+
meshes_with_colors = []
|
|
435
|
+
|
|
436
|
+
# PyVistaプロッターから全てのアクターを取得
|
|
437
|
+
renderer = self.plotter.renderer
|
|
438
|
+
actors = renderer.actors
|
|
439
|
+
|
|
440
|
+
actor_count = 0
|
|
441
|
+
for actor_name, actor in actors.items():
|
|
442
|
+
try:
|
|
443
|
+
# VTKアクターからポリデータを取得
|
|
444
|
+
mesh = None
|
|
445
|
+
|
|
446
|
+
# 方法1: mapperのinputから取得
|
|
447
|
+
if hasattr(actor, 'mapper') and actor.mapper is not None:
|
|
448
|
+
if hasattr(actor.mapper, 'input') and actor.mapper.input is not None:
|
|
449
|
+
mesh = actor.mapper.input
|
|
450
|
+
elif hasattr(actor.mapper, 'GetInput') and actor.mapper.GetInput() is not None:
|
|
451
|
+
mesh = actor.mapper.GetInput()
|
|
452
|
+
|
|
453
|
+
# 方法2: PyVistaプロッターの内部データから取得
|
|
454
|
+
if mesh is None and actor_name in self.plotter.mesh:
|
|
455
|
+
mesh = self.plotter.mesh[actor_name]
|
|
456
|
+
|
|
457
|
+
if mesh is not None and hasattr(mesh, 'n_points') and mesh.n_points > 0:
|
|
458
|
+
# PyVistaメッシュに変換(必要な場合)
|
|
459
|
+
if not isinstance(mesh, pv.PolyData):
|
|
460
|
+
if hasattr(mesh, 'extract_surface'):
|
|
461
|
+
mesh = mesh.extract_surface()
|
|
462
|
+
else:
|
|
463
|
+
mesh = pv.wrap(mesh)
|
|
464
|
+
|
|
465
|
+
# アクターから色情報を取得
|
|
466
|
+
color = [128, 128, 128] # デフォルト色(グレー)
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
# VTKアクターのプロパティから色を取得
|
|
470
|
+
if hasattr(actor, 'prop') and actor.prop is not None:
|
|
471
|
+
vtk_color = actor.prop.GetColor()
|
|
472
|
+
color = [int(c * 255) for c in vtk_color]
|
|
473
|
+
elif hasattr(actor, 'GetProperty'):
|
|
474
|
+
prop = actor.GetProperty()
|
|
475
|
+
if prop is not None:
|
|
476
|
+
vtk_color = prop.GetColor()
|
|
477
|
+
color = [int(c * 255) for c in vtk_color]
|
|
478
|
+
except:
|
|
479
|
+
# 色取得に失敗した場合はデフォルト色をそのまま使用
|
|
480
|
+
pass
|
|
481
|
+
|
|
482
|
+
# メッシュのコピーを作成
|
|
483
|
+
mesh_copy = mesh.copy()
|
|
484
|
+
|
|
485
|
+
# もしメッシュに頂点ごとの色情報が含まれている場合、
|
|
486
|
+
# それぞれの色ごとにサブメッシュに分割して個別マテリアルを作る。
|
|
487
|
+
# これにより、glyphs(すべての原子が一つのメッシュにまとめられる場合)でも
|
|
488
|
+
# 各原子の色を保持してOBJ/MTLへ出力できる。
|
|
489
|
+
try:
|
|
490
|
+
colors = None
|
|
491
|
+
pd = mesh_copy.point_data
|
|
492
|
+
# 優先的にred/green/blue配列を使用
|
|
493
|
+
if 'red' in pd and 'green' in pd and 'blue' in pd:
|
|
494
|
+
r = np.asarray(pd['red']).reshape(-1)
|
|
495
|
+
g = np.asarray(pd['green']).reshape(-1)
|
|
496
|
+
b = np.asarray(pd['blue']).reshape(-1)
|
|
497
|
+
colors = np.vstack([r, g, b]).T
|
|
498
|
+
# diffuse_* のキーもサポート
|
|
499
|
+
elif 'diffuse_red' in pd and 'diffuse_green' in pd and 'diffuse_blue' in pd:
|
|
500
|
+
r = np.asarray(pd['diffuse_red']).reshape(-1)
|
|
501
|
+
g = np.asarray(pd['diffuse_green']).reshape(-1)
|
|
502
|
+
b = np.asarray(pd['diffuse_blue']).reshape(-1)
|
|
503
|
+
colors = np.vstack([r, g, b]).T
|
|
504
|
+
# 単一の colors 配列があればそれを使う
|
|
505
|
+
elif 'colors' in pd:
|
|
506
|
+
colors = np.asarray(pd['colors'])
|
|
507
|
+
|
|
508
|
+
if colors is not None and colors.size > 0:
|
|
509
|
+
# 整数に変換。colors が 0-1 の float の場合は 255 倍して正規化する。
|
|
510
|
+
colors_arr = np.asarray(colors)
|
|
511
|
+
# 期待形状に整形
|
|
512
|
+
if colors_arr.ndim == 1:
|
|
513
|
+
# 1次元の場合は単一チャンネルとして扱う
|
|
514
|
+
colors_arr = colors_arr.reshape(-1, 1)
|
|
515
|
+
|
|
516
|
+
# float かどうか判定して正規化
|
|
517
|
+
if np.issubdtype(colors_arr.dtype, np.floating):
|
|
518
|
+
# 値の最大が1付近なら0-1レンジとみなして255倍
|
|
519
|
+
if colors_arr.max() <= 1.01:
|
|
520
|
+
colors_int = np.clip((colors_arr * 255.0).round(), 0, 255).astype(np.int32)
|
|
521
|
+
else:
|
|
522
|
+
# 既に0-255レンジのfloatならそのまま丸める
|
|
523
|
+
colors_int = np.clip(colors_arr.round(), 0, 255).astype(np.int32)
|
|
524
|
+
else:
|
|
525
|
+
colors_int = np.clip(colors_arr, 0, 255).astype(np.int32)
|
|
526
|
+
# Ensure shape is (n_points, 3)
|
|
527
|
+
if colors_int.ndim == 1:
|
|
528
|
+
# 単一値が入っている場合は同一RGBとして扱う
|
|
529
|
+
colors_int = np.vstack([colors_int, colors_int, colors_int]).T
|
|
530
|
+
|
|
531
|
+
# 一意な色ごとにサブメッシュを抽出して追加
|
|
532
|
+
unique_colors, inverse = np.unique(colors_int, axis=0, return_inverse=True)
|
|
533
|
+
if unique_colors.shape[0] > 1:
|
|
534
|
+
for uc_idx, uc in enumerate(unique_colors):
|
|
535
|
+
point_inds = np.where(inverse == uc_idx)[0]
|
|
536
|
+
if point_inds.size == 0:
|
|
537
|
+
continue
|
|
538
|
+
try:
|
|
539
|
+
submesh = mesh_copy.extract_points(point_inds, adjacent_cells=True)
|
|
540
|
+
except Exception:
|
|
541
|
+
# extract_points が利用できない場合はスキップ
|
|
542
|
+
continue
|
|
543
|
+
if submesh is None or getattr(submesh, 'n_points', 0) == 0:
|
|
544
|
+
continue
|
|
545
|
+
color_rgb = [int(uc[0]), int(uc[1]), int(uc[2])]
|
|
546
|
+
meshes_with_colors.append({
|
|
547
|
+
'mesh': submesh,
|
|
548
|
+
'color': color_rgb,
|
|
549
|
+
'name': f'{actor_name}_color_{uc_idx}',
|
|
550
|
+
'type': 'display_actor',
|
|
551
|
+
'actor_name': actor_name
|
|
552
|
+
})
|
|
553
|
+
actor_count += 1
|
|
554
|
+
# 分割したので以下の通常追加は行わない
|
|
555
|
+
continue
|
|
556
|
+
except Exception:
|
|
557
|
+
# 分割処理に失敗した場合はフォールバックで単体メッシュを追加
|
|
558
|
+
pass
|
|
559
|
+
|
|
560
|
+
meshes_with_colors.append({
|
|
561
|
+
'mesh': mesh_copy,
|
|
562
|
+
'color': color,
|
|
563
|
+
'name': f'actor_{actor_count}_{actor_name}',
|
|
564
|
+
'type': 'display_actor',
|
|
565
|
+
'actor_name': actor_name
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
actor_count += 1
|
|
569
|
+
|
|
570
|
+
except Exception as e:
|
|
571
|
+
print(f"Error processing actor {actor_name}: {e}")
|
|
572
|
+
continue
|
|
573
|
+
|
|
574
|
+
return meshes_with_colors
|
|
575
|
+
|
|
576
|
+
except Exception as e:
|
|
577
|
+
print(f"Error in export_from_3d_view_with_colors: {e}")
|
|
578
|
+
return []
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def export_2d_png(self):
|
|
583
|
+
if not self.data.atoms:
|
|
584
|
+
self.statusBar().showMessage("Nothing to export.")
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
# default filename: based on current file, append -2d for 2D exports
|
|
588
|
+
default_name = "untitled-2d"
|
|
589
|
+
try:
|
|
590
|
+
if self.current_file_path:
|
|
591
|
+
base = os.path.basename(self.current_file_path)
|
|
592
|
+
name = os.path.splitext(base)[0]
|
|
593
|
+
default_name = f"{name}-2d"
|
|
594
|
+
except Exception:
|
|
595
|
+
default_name = "untitled-2d"
|
|
596
|
+
|
|
597
|
+
# prefer same directory as current file when available
|
|
598
|
+
default_path = default_name
|
|
599
|
+
try:
|
|
600
|
+
if self.current_file_path:
|
|
601
|
+
default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
|
|
602
|
+
except Exception:
|
|
603
|
+
default_path = default_name
|
|
604
|
+
|
|
605
|
+
filePath, _ = QFileDialog.getSaveFileName(self, "Export 2D as PNG", default_path, "PNG Files (*.png)")
|
|
606
|
+
if not filePath:
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
if not (filePath.lower().endswith(".png")):
|
|
610
|
+
filePath += ".png"
|
|
611
|
+
|
|
612
|
+
reply = QMessageBox.question(self, 'Choose Background',
|
|
613
|
+
'Do you want a transparent background?\n(Choose "No" for a white background)',
|
|
614
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel,
|
|
615
|
+
QMessageBox.StandardButton.Yes)
|
|
616
|
+
|
|
617
|
+
if reply == QMessageBox.StandardButton.Cancel:
|
|
618
|
+
self.statusBar().showMessage("Export cancelled.", 2000)
|
|
619
|
+
return
|
|
620
|
+
|
|
621
|
+
is_transparent = (reply == QMessageBox.StandardButton.Yes)
|
|
622
|
+
|
|
623
|
+
QApplication.processEvents()
|
|
624
|
+
|
|
625
|
+
items_to_restore = {}
|
|
626
|
+
original_background = self.scene.backgroundBrush()
|
|
627
|
+
|
|
628
|
+
try:
|
|
629
|
+
all_items = list(self.scene.items())
|
|
630
|
+
for item in all_items:
|
|
631
|
+
is_mol_part = isinstance(item, (AtomItem, BondItem))
|
|
632
|
+
if not (is_mol_part and item.isVisible()):
|
|
633
|
+
items_to_restore[item] = item.isVisible()
|
|
634
|
+
item.hide()
|
|
635
|
+
|
|
636
|
+
molecule_bounds = QRectF()
|
|
637
|
+
for item in self.scene.items():
|
|
638
|
+
if isinstance(item, (AtomItem, BondItem)) and item.isVisible():
|
|
639
|
+
molecule_bounds = molecule_bounds.united(item.sceneBoundingRect())
|
|
640
|
+
|
|
641
|
+
if molecule_bounds.isEmpty() or not molecule_bounds.isValid():
|
|
642
|
+
self.statusBar().showMessage("Error: Could not determine molecule bounds for export.")
|
|
643
|
+
return
|
|
644
|
+
|
|
645
|
+
if is_transparent:
|
|
646
|
+
self.scene.setBackgroundBrush(QBrush(Qt.BrushStyle.NoBrush))
|
|
647
|
+
else:
|
|
648
|
+
self.scene.setBackgroundBrush(QBrush(QColor("#FFFFFF")))
|
|
649
|
+
|
|
650
|
+
rect_to_render = molecule_bounds.adjusted(-20, -20, 20, 20)
|
|
651
|
+
|
|
652
|
+
w = max(1, int(math.ceil(rect_to_render.width())))
|
|
653
|
+
h = max(1, int(math.ceil(rect_to_render.height())))
|
|
654
|
+
|
|
655
|
+
if w <= 0 or h <= 0:
|
|
656
|
+
self.statusBar().showMessage("Error: Invalid image size calculated.")
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
image = QImage(w, h, QImage.Format.Format_ARGB32_Premultiplied)
|
|
660
|
+
if is_transparent:
|
|
661
|
+
image.fill(Qt.GlobalColor.transparent)
|
|
662
|
+
else:
|
|
663
|
+
image.fill(Qt.GlobalColor.white)
|
|
664
|
+
|
|
665
|
+
painter = QPainter()
|
|
666
|
+
ok = painter.begin(image)
|
|
667
|
+
if not ok or not painter.isActive():
|
|
668
|
+
self.statusBar().showMessage("Failed to start QPainter for image rendering.")
|
|
669
|
+
return
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
673
|
+
target_rect = QRectF(0, 0, w, h)
|
|
674
|
+
source_rect = rect_to_render
|
|
675
|
+
self.scene.render(painter, target_rect, source_rect)
|
|
676
|
+
finally:
|
|
677
|
+
painter.end()
|
|
678
|
+
|
|
679
|
+
saved = image.save(filePath, "PNG")
|
|
680
|
+
if saved:
|
|
681
|
+
self.statusBar().showMessage(f"2D view exported to {filePath}")
|
|
682
|
+
else:
|
|
683
|
+
self.statusBar().showMessage("Failed to save image. Check file path or permissions.")
|
|
684
|
+
|
|
685
|
+
except Exception as e:
|
|
686
|
+
self.statusBar().showMessage(f"An unexpected error occurred during 2D export: {e}")
|
|
687
|
+
|
|
688
|
+
finally:
|
|
689
|
+
for item, was_visible in items_to_restore.items():
|
|
690
|
+
item.setVisible(was_visible)
|
|
691
|
+
self.scene.setBackgroundBrush(original_background)
|
|
692
|
+
if self.view_2d:
|
|
693
|
+
self.view_2d.viewport().update()
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def export_3d_png(self):
|
|
698
|
+
if not self.current_mol:
|
|
699
|
+
self.statusBar().showMessage("No 3D molecule to export.", 2000)
|
|
700
|
+
return
|
|
701
|
+
|
|
702
|
+
# default filename: match XYZ/MOL naming (use base name without suffix)
|
|
703
|
+
default_name = "untitled"
|
|
704
|
+
try:
|
|
705
|
+
if self.current_file_path:
|
|
706
|
+
base = os.path.basename(self.current_file_path)
|
|
707
|
+
name = os.path.splitext(base)[0]
|
|
708
|
+
default_name = f"{name}"
|
|
709
|
+
except Exception:
|
|
710
|
+
default_name = "untitled"
|
|
711
|
+
|
|
712
|
+
# prefer same directory as current file when available
|
|
713
|
+
default_path = default_name
|
|
714
|
+
try:
|
|
715
|
+
if self.current_file_path:
|
|
716
|
+
default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
|
|
717
|
+
except Exception:
|
|
718
|
+
default_path = default_name
|
|
719
|
+
|
|
720
|
+
filePath, _ = QFileDialog.getSaveFileName(self, "Export 3D as PNG", default_path, "PNG Files (*.png)")
|
|
721
|
+
if not filePath:
|
|
722
|
+
return
|
|
723
|
+
|
|
724
|
+
if not (filePath.lower().endswith(".png")):
|
|
725
|
+
filePath += ".png"
|
|
726
|
+
|
|
727
|
+
reply = QMessageBox.question(self, 'Choose Background',
|
|
728
|
+
'Do you want a transparent background?\n(Choose "No" for current background)',
|
|
729
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel,
|
|
730
|
+
QMessageBox.StandardButton.Yes)
|
|
731
|
+
|
|
732
|
+
if reply == QMessageBox.StandardButton.Cancel:
|
|
733
|
+
self.statusBar().showMessage("Export cancelled.", 2000)
|
|
734
|
+
return
|
|
735
|
+
|
|
736
|
+
is_transparent = (reply == QMessageBox.StandardButton.Yes)
|
|
737
|
+
|
|
738
|
+
try:
|
|
739
|
+
self.plotter.screenshot(filePath, transparent_background=is_transparent)
|
|
740
|
+
self.statusBar().showMessage(f"3D view exported to {filePath}", 3000)
|
|
741
|
+
except Exception as e:
|
|
742
|
+
self.statusBar().showMessage(f"Error exporting 3D PNG: {e}")
|
|
743
|
+
|
|
744
|
+
|