MoleditPy-linux 2.2.4__py3-none-any.whl

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