MoleditPy-linux 1.17.1__py3-none-any.whl → 1.18.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. moleditpy_linux/modules/align_plane_dialog.py +1 -1
  2. moleditpy_linux/modules/alignment_dialog.py +1 -1
  3. moleditpy_linux/modules/angle_dialog.py +2 -2
  4. moleditpy_linux/modules/bond_item.py +96 -5
  5. moleditpy_linux/modules/bond_length_dialog.py +2 -2
  6. moleditpy_linux/modules/constants.py +1 -1
  7. moleditpy_linux/modules/constrained_optimization_dialog.py +2 -2
  8. moleditpy_linux/modules/dihedral_dialog.py +1 -1
  9. moleditpy_linux/modules/main_window_app_state.py +1 -1
  10. moleditpy_linux/modules/main_window_compute.py +6 -0
  11. moleditpy_linux/modules/main_window_edit_3d.py +7 -7
  12. moleditpy_linux/modules/main_window_export.py +106 -49
  13. moleditpy_linux/modules/main_window_main_init.py +3 -3
  14. moleditpy_linux/modules/main_window_molecular_parsers.py +4 -3
  15. moleditpy_linux/modules/main_window_project_io.py +2 -2
  16. moleditpy_linux/modules/main_window_view_3d.py +359 -131
  17. moleditpy_linux/modules/main_window_view_loaders.py +1 -1
  18. moleditpy_linux/modules/molecule_scene.py +14 -15
  19. moleditpy_linux/modules/planarize_dialog.py +1 -1
  20. moleditpy_linux/modules/settings_dialog.py +86 -20
  21. moleditpy_linux/modules/user_template_dialog.py +9 -8
  22. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/METADATA +1 -2
  23. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/RECORD +27 -27
  24. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/WHEEL +0 -0
  25. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/entry_points.txt +0 -0
  26. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/licenses/LICENSE +0 -0
  27. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/top_level.txt +0 -0
@@ -194,7 +194,7 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
194
194
  for label_actor in self.selection_labels:
195
195
  try:
196
196
  self.main_window.plotter.remove_actor(label_actor)
197
- except:
197
+ except Exception:
198
198
  pass
199
199
  self.selection_labels = []
200
200
 
@@ -166,7 +166,7 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
166
166
  for label_actor in self.selection_labels:
167
167
  try:
168
168
  self.main_window.plotter.remove_actor(label_actor)
169
- except:
169
+ except Exception:
170
170
  pass
171
171
  self.selection_labels = []
172
172
 
@@ -202,7 +202,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
202
202
  for label_actor in self.selection_labels:
203
203
  try:
204
204
  self.main_window.plotter.remove_actor(label_actor)
205
- except:
205
+ except Exception:
206
206
  pass
207
207
  self.selection_labels = []
208
208
 
@@ -212,7 +212,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
212
212
  for label_actor in self.selection_labels:
213
213
  try:
214
214
  self.main_window.plotter.remove_actor(label_actor)
215
- except:
215
+ except Exception:
216
216
  pass
217
217
  self.selection_labels = []
218
218
 
@@ -216,11 +216,102 @@ class BondItem(QGraphicsItem):
216
216
  offset = QPointF(v.dx(), v.dy()) * BOND_OFFSET
217
217
 
218
218
  if self.order == 2:
219
- # -------------------- ここから差し替え --------------------)
220
- line1 = line.translated(offset)
221
- line2 = line.translated(-offset)
222
- painter.drawLine(line1)
223
- painter.drawLine(line2)
219
+ # 環構造かどうかを判定し、描画方法を変更
220
+ is_in_ring = False
221
+ ring_center = None
222
+
223
+ try:
224
+ # シーンからRDKit分子を取得
225
+ sc = self.scene()
226
+ if sc and hasattr(sc, 'window') and sc.window:
227
+ # 2DデータからRDKit分子を生成
228
+ mol = sc.window.data.to_rdkit_mol(use_2d_stereo=False)
229
+ if mol:
230
+ # この結合に対応するRDKitボンドを探す
231
+ atom1_id = self.atom1.atom_id
232
+ atom2_id = self.atom2.atom_id
233
+
234
+ # RDKitインデックスを取得
235
+ rdkit_idx1 = None
236
+ rdkit_idx2 = None
237
+ for atom in mol.GetAtoms():
238
+ if atom.HasProp("_original_atom_id"):
239
+ orig_id = atom.GetIntProp("_original_atom_id")
240
+ if orig_id == atom1_id:
241
+ rdkit_idx1 = atom.GetIdx()
242
+ elif orig_id == atom2_id:
243
+ rdkit_idx2 = atom.GetIdx()
244
+
245
+ if rdkit_idx1 is not None and rdkit_idx2 is not None:
246
+ bond = mol.GetBondBetweenAtoms(rdkit_idx1, rdkit_idx2)
247
+ if bond and bond.IsInRing():
248
+ is_in_ring = True
249
+ # 環の中心を計算(この結合を含む最小環)
250
+ from rdkit import Chem
251
+ ring_info = mol.GetRingInfo()
252
+ for ring in ring_info.AtomRings():
253
+ if rdkit_idx1 in ring and rdkit_idx2 in ring:
254
+ # 環の原子位置の平均を計算
255
+ ring_positions = []
256
+ for atom_idx in ring:
257
+ # 対応するエディタ側の原子を探す
258
+ rdkit_atom = mol.GetAtomWithIdx(atom_idx)
259
+ if rdkit_atom.HasProp("_original_atom_id"):
260
+ editor_atom_id = rdkit_atom.GetIntProp("_original_atom_id")
261
+ if editor_atom_id in sc.window.data.atoms:
262
+ atom_item = sc.window.data.atoms[editor_atom_id]['item']
263
+ if atom_item:
264
+ ring_positions.append(atom_item.pos())
265
+
266
+ if ring_positions:
267
+ # 環の中心を計算
268
+ center_x = sum(p.x() for p in ring_positions) / len(ring_positions)
269
+ center_y = sum(p.y() for p in ring_positions) / len(ring_positions)
270
+ ring_center = QPointF(center_x, center_y)
271
+ break
272
+ except Exception as e:
273
+ # エラーが発生した場合は通常の描画にフォールバック
274
+ is_in_ring = False
275
+
276
+ v = line.unitVector().normalVector()
277
+ offset = QPointF(v.dx(), v.dy()) * BOND_OFFSET
278
+
279
+ if is_in_ring and ring_center:
280
+ # 環構造: 1本の中心線(単結合位置) + 1本の短い内側線
281
+ # 結合の中心から環の中心への方向を計算
282
+ bond_center = line.center()
283
+
284
+ # ローカル座標系での環中心方向
285
+ local_ring_center = self.mapFromScene(ring_center)
286
+ local_bond_center = line.center()
287
+ inward_vec = local_ring_center - local_bond_center
288
+
289
+ # offsetとinward_vecの内積で内側を判定
290
+ if QPointF.dotProduct(offset, inward_vec) > 0:
291
+ # offsetが内側方向(2倍のオフセット)
292
+ inner_offset = offset * 2
293
+ else:
294
+ # -offsetが内側方向(2倍のオフセット)
295
+ inner_offset = -offset * 2
296
+
297
+ # 中心線を描画(単結合と同じ位置)
298
+ painter.drawLine(line)
299
+
300
+ # 内側の短い線を描画(80%の長さ)
301
+ inner_line = line.translated(inner_offset)
302
+ shorten_factor = 0.8
303
+ p1 = inner_line.p1()
304
+ p2 = inner_line.p2()
305
+ center = QPointF((p1.x() + p2.x()) / 2, (p1.y() + p2.y()) / 2)
306
+ shortened_p1 = center + (p1 - center) * shorten_factor
307
+ shortened_p2 = center + (p2 - center) * shorten_factor
308
+ painter.drawLine(QLineF(shortened_p1, shortened_p2))
309
+ else:
310
+ # 非環構造: 従来の2本の平行線
311
+ line1 = line.translated(offset)
312
+ line2 = line.translated(-offset)
313
+ painter.drawLine(line1)
314
+ painter.drawLine(line2)
224
315
 
225
316
  # E/Z ラベルの描画処理
226
317
  if self.stereo in [3, 4]:
@@ -190,7 +190,7 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
190
190
  for label_actor in self.selection_labels:
191
191
  try:
192
192
  self.main_window.plotter.remove_actor(label_actor)
193
- except:
193
+ except Exception:
194
194
  pass
195
195
  self.selection_labels = []
196
196
 
@@ -200,7 +200,7 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
200
200
  for label_actor in self.selection_labels:
201
201
  try:
202
202
  self.main_window.plotter.remove_actor(label_actor)
203
- except:
203
+ except Exception:
204
204
  pass
205
205
  self.selection_labels = []
206
206
 
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QFont, QColor
16
16
  from rdkit import Chem
17
17
 
18
18
  #Version
19
- VERSION = '1.17.1'
19
+ VERSION = '1.18.1'
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -394,7 +394,7 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
394
394
  for label_actor in self.constraint_labels:
395
395
  try:
396
396
  self.main_window.plotter.remove_actor(label_actor)
397
- except:
397
+ except Exception:
398
398
  pass
399
399
  self.constraint_labels = []
400
400
 
@@ -595,7 +595,7 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
595
595
  for label_actor in self.selection_labels:
596
596
  try:
597
597
  self.main_window.plotter.remove_actor(label_actor)
598
- except:
598
+ except Exception:
599
599
  pass
600
600
  self.selection_labels = []
601
601
 
@@ -207,7 +207,7 @@ class DihedralDialog(Dialog3DPickingMixin, QDialog):
207
207
  for label_actor in self.selection_labels:
208
208
  try:
209
209
  self.main_window.plotter.remove_actor(label_actor)
210
- except:
210
+ except Exception:
211
211
  pass
212
212
  self.selection_labels = []
213
213
 
@@ -574,7 +574,7 @@ class MainWindowAppState(object):
574
574
  inchi_key = Chem.MolToInchiKey(self.current_mol)
575
575
  json_data["identifiers"]["inchi"] = inchi
576
576
  json_data["identifiers"]["inchi_key"] = inchi_key
577
- except:
577
+ except Exception:
578
578
  pass # InChI生成に失敗した場合は無視
579
579
 
580
580
  except Exception as e:
@@ -958,6 +958,12 @@ class MainWindowCompute(object):
958
958
  try:
959
959
  if mol.GetNumConformers() > 0:
960
960
  # 初回変換では、2Dで設定したwedge/dashボンドの立体情報を保持
961
+
962
+ # 3D立体化学計算で上書きされる前に、2D由来の立体化学情報をプロパティとして保存
963
+ for bond in mol.GetBonds():
964
+ if bond.GetBondType() == Chem.BondType.DOUBLE:
965
+ bond.SetIntProp("_original_2d_stereo", bond.GetStereo())
966
+
961
967
  # 立体化学の割り当てを行うが、既存の2D立体情報を尊重
962
968
  Chem.AssignStereochemistry(mol, cleanIt=False, force=True)
963
969
 
@@ -119,7 +119,7 @@ class MainWindowEdit3d(object):
119
119
  for dialog in dialogs_to_close:
120
120
  try:
121
121
  dialog.close()
122
- except:
122
+ except Exception:
123
123
  pass
124
124
  self.active_3d_dialogs.clear()
125
125
 
@@ -169,7 +169,7 @@ class MainWindowEdit3d(object):
169
169
  try:
170
170
  # 既存の測定ラベルを削除
171
171
  self.plotter.remove_actor('measurement_labels')
172
- except:
172
+ except Exception:
173
173
  pass
174
174
 
175
175
  if not self.measurement_labels or not self.current_mol:
@@ -208,7 +208,7 @@ class MainWindowEdit3d(object):
208
208
  self.measurement_labels.clear()
209
209
  try:
210
210
  self.plotter.remove_actor('measurement_labels')
211
- except:
211
+ except Exception:
212
212
  pass
213
213
 
214
214
  # 2Dビューの測定ラベルも削除
@@ -219,7 +219,7 @@ class MainWindowEdit3d(object):
219
219
  try:
220
220
  self.plotter.remove_actor(self.measurement_text_actor)
221
221
  self.measurement_text_actor = None
222
- except:
222
+ except Exception:
223
223
  pass
224
224
 
225
225
  self.plotter.render()
@@ -434,7 +434,7 @@ class MainWindowEdit3d(object):
434
434
  if self.measurement_text_actor:
435
435
  try:
436
436
  self.plotter.remove_actor(self.measurement_text_actor)
437
- except:
437
+ except Exception:
438
438
  pass
439
439
 
440
440
  if not measurement_lines:
@@ -453,7 +453,7 @@ class MainWindowEdit3d(object):
453
453
  text_color = 'black' if luminance > 128 else 'white'
454
454
  else:
455
455
  text_color = 'white'
456
- except:
456
+ except Exception:
457
457
  text_color = 'white'
458
458
 
459
459
  # 左上に表示(小さな等幅フォント)
@@ -496,7 +496,7 @@ class MainWindowEdit3d(object):
496
496
  try:
497
497
  # 既存の選択ハイライトを削除
498
498
  self.plotter.remove_actor('selection_highlight')
499
- except:
499
+ except Exception:
500
500
  pass
501
501
 
502
502
  if not self.selected_atoms_3d or not self.current_mol:
@@ -178,11 +178,6 @@ class MainWindowExport(object):
178
178
  except Exception as e:
179
179
  self.statusBar().showMessage(f"Error exporting OBJ/MTL: {e}")
180
180
 
181
- return meshes_with_colors
182
-
183
- except Exception:
184
- return []
185
-
186
181
 
187
182
 
188
183
  def create_multi_material_obj(self, meshes_with_colors, obj_path, mtl_path):
@@ -199,17 +194,17 @@ class MainWindowExport(object):
199
194
  material_name = f"material_{i}_{mesh_data['name'].replace(' ', '_')}"
200
195
 
201
196
  mtl_file.write(f"newmtl {material_name}\n")
202
- mtl_file.write("Ka 0.2 0.2 0.2\n") # Ambient
197
+ mtl_file.write(f"Ka 0.2 0.2 0.2\n") # Ambient
203
198
  mtl_file.write(f"Kd {color[0]/255.0:.3f} {color[1]/255.0:.3f} {color[2]/255.0:.3f}\n") # Diffuse
204
- mtl_file.write("Ks 0.5 0.5 0.5\n") # Specular
205
- mtl_file.write("Ns 32.0\n") # Specular exponent
206
- mtl_file.write("illum 2\n") # Illumination model
207
- mtl_file.write("\n")
199
+ mtl_file.write(f"Ks 0.5 0.5 0.5\n") # Specular
200
+ mtl_file.write(f"Ns 32.0\n") # Specular exponent
201
+ mtl_file.write(f"illum 2\n") # Illumination model
202
+ mtl_file.write(f"\n")
208
203
 
209
204
  # OBJファイルを作成
210
205
  with open(obj_path, 'w') as obj_file:
211
- obj_file.write("# OBJ file with multiple materials\n")
212
- obj_file.write("# Generated with individual object colors\n")
206
+ obj_file.write(f"# OBJ file with multiple materials\n")
207
+ obj_file.write(f"# Generated with individual object colors\n")
213
208
  obj_file.write(f"mtllib {os.path.basename(mtl_path)}\n\n")
214
209
 
215
210
  vertex_offset = 1 # OBJファイルの頂点インデックスは1から始まる
@@ -229,6 +224,7 @@ class MainWindowExport(object):
229
224
  obj_file.write(f"v {point[0]:.6f} {point[1]:.6f} {point[2]:.6f}\n")
230
225
 
231
226
  # 面を書き込み
227
+ faces_written = 0
232
228
  for j in range(mesh.n_cells):
233
229
  cell = mesh.get_cell(j)
234
230
  if cell.type == 5: # VTK_TRIANGLE
@@ -237,6 +233,25 @@ class MainWindowExport(object):
237
233
  v2 = points_in_cell[1] + vertex_offset
238
234
  v3 = points_in_cell[2] + vertex_offset
239
235
  obj_file.write(f"f {v1} {v2} {v3}\n")
236
+ faces_written += 1
237
+ elif cell.type == 6: # VTK_TRIANGLE_STRIP
238
+ # Triangle strips share vertices between adjacent triangles
239
+ # For n points, we get (n-2) triangles
240
+ points_in_cell = cell.point_ids
241
+ n_points = len(points_in_cell)
242
+ for k in range(n_points - 2):
243
+ if k % 2 == 0:
244
+ # Even triangles: use points k, k+1, k+2
245
+ v1 = points_in_cell[k] + vertex_offset
246
+ v2 = points_in_cell[k+1] + vertex_offset
247
+ v3 = points_in_cell[k+2] + vertex_offset
248
+ else:
249
+ # Odd triangles: reverse winding to maintain consistent orientation
250
+ v1 = points_in_cell[k+1] + vertex_offset
251
+ v2 = points_in_cell[k] + vertex_offset
252
+ v3 = points_in_cell[k+2] + vertex_offset
253
+ obj_file.write(f"f {v1} {v2} {v3}\n")
254
+ faces_written += 1
240
255
  elif cell.type == 9: # VTK_QUAD
241
256
  points_in_cell = cell.point_ids
242
257
  v1 = points_in_cell[0] + vertex_offset
@@ -244,10 +259,12 @@ class MainWindowExport(object):
244
259
  v3 = points_in_cell[2] + vertex_offset
245
260
  v4 = points_in_cell[3] + vertex_offset
246
261
  obj_file.write(f"f {v1} {v2} {v3} {v4}\n")
262
+ faces_written += 1
247
263
 
248
- vertex_offset += mesh.n_points
249
- obj_file.write("\n")
250
264
 
265
+ vertex_offset += mesh.n_points
266
+ obj_file.write(f"\n")
267
+
251
268
  except Exception as e:
252
269
  raise Exception(f"Failed to create multi-material OBJ: {e}")
253
270
 
@@ -310,24 +327,25 @@ class MainWindowExport(object):
310
327
  # VTKアクターからポリデータを取得する複数の方法を試行
311
328
  mesh = None
312
329
 
313
- # 方法1: mapperのinputから取得
330
+ # 方法1: mapperのinputから取得 (Improved)
331
+ mapper = None
314
332
  if hasattr(actor, 'mapper') and actor.mapper is not None:
315
- if hasattr(actor.mapper, 'input') and actor.mapper.input is not None:
316
- mesh = actor.mapper.input
317
- elif hasattr(actor.mapper, 'GetInput') and actor.mapper.GetInput() is not None:
318
- mesh = actor.mapper.GetInput()
333
+ mapper = actor.mapper
334
+ elif hasattr(actor, 'GetMapper'):
335
+ mapper = actor.GetMapper()
336
+
337
+ if mapper is not None:
338
+ if hasattr(mapper, 'input') and mapper.input is not None:
339
+ mesh = mapper.input
340
+ elif hasattr(mapper, 'GetInput') and mapper.GetInput() is not None:
341
+ mesh = mapper.GetInput()
342
+ elif hasattr(mapper, 'GetInputAsDataSet'):
343
+ mesh = mapper.GetInputAsDataSet()
319
344
 
320
345
  # 方法2: PyVistaプロッターの内部データから取得
321
346
  if mesh is None and actor_name in self.plotter.mesh:
322
347
  mesh = self.plotter.mesh[actor_name]
323
348
 
324
- # 方法3: PyVistaのメッシュデータベースから検索
325
- if mesh is None:
326
- for mesh_name, mesh_data in self.plotter.mesh.items():
327
- if mesh_data is not None and mesh_data.n_points > 0:
328
- mesh = mesh_data
329
- break
330
-
331
349
  if mesh is not None and hasattr(mesh, 'n_points') and mesh.n_points > 0:
332
350
  # PyVistaメッシュに変換(必要な場合)
333
351
  if not isinstance(mesh, pv.PolyData):
@@ -391,23 +409,26 @@ class MainWindowExport(object):
391
409
  # VTKアクターからポリデータを取得する複数の方法を試行
392
410
  mesh = None
393
411
 
394
- # 方法1: mapperのinputから取得
412
+ # 方法1: mapperのinputから取得 (Improved)
413
+ mapper = None
395
414
  if hasattr(actor, 'mapper') and actor.mapper is not None:
396
- if hasattr(actor.mapper, 'input') and actor.mapper.input is not None:
397
- mesh = actor.mapper.input
398
- elif hasattr(actor.mapper, 'GetInput') and actor.mapper.GetInput() is not None:
399
- mesh = actor.mapper.GetInput()
415
+ mapper = actor.mapper
416
+ elif hasattr(actor, 'GetMapper'):
417
+ mapper = actor.GetMapper()
418
+
419
+ if mapper is not None:
420
+ if hasattr(mapper, 'input') and mapper.input is not None:
421
+ mesh = mapper.input
422
+ elif hasattr(mapper, 'GetInput') and mapper.GetInput() is not None:
423
+ mesh = mapper.GetInput()
424
+ elif hasattr(mapper, 'GetInputAsDataSet'):
425
+ mesh = mapper.GetInputAsDataSet()
400
426
 
401
427
  # 方法2: PyVistaプロッターの内部データから取得
402
428
  if mesh is None and actor_name in self.plotter.mesh:
403
429
  mesh = self.plotter.mesh[actor_name]
404
430
 
405
- # 方法3: PyVistaのメッシュデータベースから検索
406
- if mesh is None:
407
- for mesh_name, mesh_data in self.plotter.mesh.items():
408
- if mesh_data is not None and mesh_data.n_points > 0:
409
- mesh = mesh_data
410
- break
431
+ # 方法3: Removed unsafe fallback
411
432
 
412
433
  if mesh is not None and hasattr(mesh, 'n_points') and mesh.n_points > 0:
413
434
  # PyVistaメッシュに変換(必要な場合)
@@ -447,17 +468,26 @@ class MainWindowExport(object):
447
468
  actors = renderer.actors
448
469
 
449
470
  actor_count = 0
471
+
450
472
  for actor_name, actor in actors.items():
451
473
  try:
452
474
  # VTKアクターからポリデータを取得
453
475
  mesh = None
454
476
 
455
- # 方法1: mapperのinputから取得
477
+ # 方法1: mapperのinputから取得 (Improved)
478
+ mapper = None
456
479
  if hasattr(actor, 'mapper') and actor.mapper is not None:
457
- if hasattr(actor.mapper, 'input') and actor.mapper.input is not None:
458
- mesh = actor.mapper.input
459
- elif hasattr(actor.mapper, 'GetInput') and actor.mapper.GetInput() is not None:
460
- mesh = actor.mapper.GetInput()
480
+ mapper = actor.mapper
481
+ elif hasattr(actor, 'GetMapper'):
482
+ mapper = actor.GetMapper()
483
+
484
+ if mapper is not None:
485
+ if hasattr(mapper, 'input') and mapper.input is not None:
486
+ mesh = mapper.input
487
+ elif hasattr(mapper, 'GetInput') and mapper.GetInput() is not None:
488
+ mesh = mapper.GetInput()
489
+ elif hasattr(mapper, 'GetInputAsDataSet'):
490
+ mesh = mapper.GetInputAsDataSet()
461
491
 
462
492
  # 方法2: PyVistaプロッターの内部データから取得
463
493
  if mesh is None and actor_name in self.plotter.mesh:
@@ -484,7 +514,7 @@ class MainWindowExport(object):
484
514
  if prop is not None:
485
515
  vtk_color = prop.GetColor()
486
516
  color = [int(c * 255) for c in vtk_color]
487
- except:
517
+ except Exception:
488
518
  # 色取得に失敗した場合はデフォルト色をそのまま使用
489
519
  pass
490
520
 
@@ -513,6 +543,16 @@ class MainWindowExport(object):
513
543
  # 単一の colors 配列があればそれを使う
514
544
  elif 'colors' in pd:
515
545
  colors = np.asarray(pd['colors'])
546
+
547
+ # cell_dataのcolorsも確認(Tubeフィルタなどはcell_dataに色を持つ場合がある)
548
+ if colors is None and 'colors' in mesh_copy.cell_data:
549
+ try:
550
+ # cell_dataをpoint_dataに変換
551
+ temp_mesh = mesh_copy.cell_data_to_point_data()
552
+ if 'colors' in temp_mesh.point_data:
553
+ colors = np.asarray(temp_mesh.point_data['colors'])
554
+ except Exception:
555
+ pass
516
556
 
517
557
  if colors is not None and colors.size > 0:
518
558
  # 整数に変換。colors が 0-1 の float の場合は 255 倍して正規化する。
@@ -539,18 +579,26 @@ class MainWindowExport(object):
539
579
 
540
580
  # 一意な色ごとにサブメッシュを抽出して追加
541
581
  unique_colors, inverse = np.unique(colors_int, axis=0, return_inverse=True)
582
+
583
+ split_success = False
542
584
  if unique_colors.shape[0] > 1:
543
585
  for uc_idx, uc in enumerate(unique_colors):
544
586
  point_inds = np.where(inverse == uc_idx)[0]
545
587
  if point_inds.size == 0:
546
588
  continue
547
589
  try:
548
- submesh = mesh_copy.extract_points(point_inds, adjacent_cells=True)
590
+ # Use temp_mesh if available (has point data), else mesh_copy
591
+ target_mesh = temp_mesh if 'temp_mesh' in locals() else mesh_copy
592
+
593
+ # extract_points with adjacent_cells=False to avoid pulling in neighbors
594
+ submesh = target_mesh.extract_points(point_inds, adjacent_cells=False)
595
+
549
596
  except Exception:
550
597
  # extract_points が利用できない場合はスキップ
551
598
  continue
552
599
  if submesh is None or getattr(submesh, 'n_points', 0) == 0:
553
600
  continue
601
+
554
602
  color_rgb = [int(uc[0]), int(uc[1]), int(uc[2])]
555
603
  meshes_with_colors.append({
556
604
  'mesh': submesh,
@@ -559,13 +607,22 @@ class MainWindowExport(object):
559
607
  'type': 'display_actor',
560
608
  'actor_name': actor_name
561
609
  })
562
- actor_count += 1
563
- # 分割したので以下の通常追加は行わない
564
- continue
610
+ split_success = True
611
+
612
+ if split_success:
613
+ actor_count += 1
614
+ # 分割に成功したので以下の通常追加は行わない
615
+ continue
616
+ # If splitting failed (no submeshes added), fall through to default
617
+ else:
618
+ # 色が1色のみの場合は、その色を使用してメッシュ全体を出力
619
+ uc = unique_colors[0]
620
+ color = [int(uc[0]), int(uc[1]), int(uc[2])]
621
+ # ここでは continue せず、下のデフォルト追加処理に任せる(colorを更新したため)
565
622
  except Exception:
566
623
  # 分割処理に失敗した場合はフォールバックで単体メッシュを追加
567
624
  pass
568
-
625
+
569
626
  meshes_with_colors.append({
570
627
  'mesh': mesh_copy,
571
628
  'color': color,
@@ -577,9 +634,9 @@ class MainWindowExport(object):
577
634
  actor_count += 1
578
635
 
579
636
  except Exception as e:
580
- print(f"Error processing actor {actor_name}: {e}")
581
637
  continue
582
638
 
639
+
583
640
  return meshes_with_colors
584
641
 
585
642
  except Exception as e:
@@ -1563,7 +1563,6 @@ class MainWindowMainInit(object):
1563
1563
  'wireframe_bond_radius': 0.01,
1564
1564
  'wireframe_resolution': 6,
1565
1565
  # Stick model parameters
1566
- 'stick_atom_radius': 0.15,
1567
1566
  'stick_bond_radius': 0.15,
1568
1567
  'stick_resolution': 16,
1569
1568
  # Multiple bond offset parameters (per-model)
@@ -1573,12 +1572,13 @@ class MainWindowMainInit(object):
1573
1572
  'ball_stick_triple_bond_radius_factor': 0.75,
1574
1573
  'wireframe_double_bond_offset_factor': 3.0,
1575
1574
  'wireframe_triple_bond_offset_factor': 3.0,
1576
- 'wireframe_double_bond_radius_factor': 1.0,
1575
+ 'wireframe_double_bond_radius_factor': 0.8,
1577
1576
  'wireframe_triple_bond_radius_factor': 0.75,
1578
1577
  'stick_double_bond_offset_factor': 1.5,
1579
1578
  'stick_triple_bond_offset_factor': 1.0,
1580
1579
  'stick_double_bond_radius_factor': 0.60,
1581
1580
  'stick_triple_bond_radius_factor': 0.40,
1581
+ 'aromatic_torus_thickness_factor': 0.6,
1582
1582
  # Ensure conversion/optimization defaults are present
1583
1583
  # If True, attempts to be permissive when RDKit raises chemical/sanitization errors
1584
1584
  # during file import (useful for viewing malformed XYZ/MOL files).
@@ -1629,7 +1629,7 @@ class MainWindowMainInit(object):
1629
1629
  ('ball_stick_triple_bond_radius_factor', 'triple_bond_radius_factor', 0.75),
1630
1630
  ('wireframe_double_bond_offset_factor', 'double_bond_offset_factor', 3.0),
1631
1631
  ('wireframe_triple_bond_offset_factor', 'triple_bond_offset_factor', 3.0),
1632
- ('wireframe_double_bond_radius_factor', 'double_bond_radius_factor', 1.0),
1632
+ ('wireframe_double_bond_radius_factor', 'double_bond_radius_factor', 0.8),
1633
1633
  ('wireframe_triple_bond_radius_factor', 'triple_bond_radius_factor', 0.75),
1634
1634
  ('stick_double_bond_offset_factor', 'double_bond_offset_factor', 1.5),
1635
1635
  ('stick_triple_bond_offset_factor', 'triple_bond_offset_factor', 1.0),
@@ -22,6 +22,7 @@ import io
22
22
  import os
23
23
  import contextlib
24
24
  import traceback
25
+ import logging
25
26
 
26
27
 
27
28
  # RDKit imports (explicit to satisfy flake8 and used features)
@@ -59,7 +60,7 @@ if OBABEL_AVAILABLE:
59
60
  # If import fails here, disable OBABEL locally; avoid raising
60
61
  pybel = None
61
62
  OBABEL_AVAILABLE = False
62
- print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
63
+ logging.warning("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
63
64
  else:
64
65
  pybel = None
65
66
 
@@ -901,12 +902,12 @@ class MainWindowMolecularParsers(object):
901
902
  try:
902
903
  mol.AddBond(i, j, Chem.BondType.SINGLE)
903
904
  bonds_added.append((i, j, distance))
904
- except:
905
+ except Exception:
905
906
  # 既に結合が存在する場合はスキップ
906
907
  pass
907
908
 
908
909
  # デバッグ情報(オプション)
909
- # print(f"Added {len(bonds_added)} bonds based on distance analysis")
910
+ # Added bonds based on distance analysis
910
911
 
911
912
  return len(bonds_added)
912
913
 
@@ -430,9 +430,9 @@ class MainWindowProjectIo(object):
430
430
  # 拡張子不明の場合はJSONとして試行
431
431
  try:
432
432
  self.load_json_data(file_path)
433
- except:
433
+ except Exception:
434
434
  try:
435
435
  self.load_raw_data(file_path)
436
- except:
436
+ except Exception:
437
437
  self.statusBar().showMessage("Error: Unable to determine file format.")
438
438