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.
Files changed (55) hide show
  1. moleditpy/__init__.py +4 -0
  2. moleditpy/__main__.py +29 -19748
  3. moleditpy/main.py +40 -0
  4. moleditpy/modules/__init__.py +36 -0
  5. moleditpy/modules/about_dialog.py +92 -0
  6. moleditpy/modules/align_plane_dialog.py +281 -0
  7. moleditpy/modules/alignment_dialog.py +261 -0
  8. moleditpy/modules/analysis_window.py +197 -0
  9. moleditpy/modules/angle_dialog.py +428 -0
  10. moleditpy/modules/assets/icon.icns +0 -0
  11. moleditpy/modules/atom_item.py +336 -0
  12. moleditpy/modules/bond_item.py +303 -0
  13. moleditpy/modules/bond_length_dialog.py +368 -0
  14. moleditpy/modules/calculation_worker.py +754 -0
  15. moleditpy/modules/color_settings_dialog.py +309 -0
  16. moleditpy/modules/constants.py +76 -0
  17. moleditpy/modules/constrained_optimization_dialog.py +667 -0
  18. moleditpy/modules/custom_interactor_style.py +737 -0
  19. moleditpy/modules/custom_qt_interactor.py +49 -0
  20. moleditpy/modules/dialog3_d_picking_mixin.py +96 -0
  21. moleditpy/modules/dihedral_dialog.py +431 -0
  22. moleditpy/modules/main_window.py +827 -0
  23. moleditpy/modules/main_window_app_state.py +709 -0
  24. moleditpy/modules/main_window_compute.py +1203 -0
  25. moleditpy/modules/main_window_dialog_manager.py +454 -0
  26. moleditpy/modules/main_window_edit_3d.py +531 -0
  27. moleditpy/modules/main_window_edit_actions.py +1449 -0
  28. moleditpy/modules/main_window_export.py +744 -0
  29. moleditpy/modules/main_window_main_init.py +1560 -0
  30. moleditpy/modules/main_window_molecular_parsers.py +956 -0
  31. moleditpy/modules/main_window_project_io.py +416 -0
  32. moleditpy/modules/main_window_string_importers.py +270 -0
  33. moleditpy/modules/main_window_ui_manager.py +567 -0
  34. moleditpy/modules/main_window_view_3d.py +1163 -0
  35. moleditpy/modules/main_window_view_loaders.py +350 -0
  36. moleditpy/modules/mirror_dialog.py +110 -0
  37. moleditpy/modules/molecular_data.py +290 -0
  38. moleditpy/modules/molecule_scene.py +1895 -0
  39. moleditpy/modules/move_group_dialog.py +586 -0
  40. moleditpy/modules/periodic_table_dialog.py +72 -0
  41. moleditpy/modules/planarize_dialog.py +209 -0
  42. moleditpy/modules/settings_dialog.py +1034 -0
  43. moleditpy/modules/template_preview_item.py +148 -0
  44. moleditpy/modules/template_preview_view.py +62 -0
  45. moleditpy/modules/translation_dialog.py +353 -0
  46. moleditpy/modules/user_template_dialog.py +621 -0
  47. moleditpy/modules/zoomable_view.py +98 -0
  48. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/METADATA +1 -1
  49. moleditpy-1.16.0a1.dist-info/RECORD +54 -0
  50. moleditpy-1.15.1.dist-info/RECORD +0 -9
  51. /moleditpy/{assets → modules/assets}/icon.ico +0 -0
  52. /moleditpy/{assets → modules/assets}/icon.png +0 -0
  53. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/WHEEL +0 -0
  54. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0a1.dist-info}/entry_points.txt +0 -0
  55. {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
+