mirror of
https://github.com/barkeser2002/VMD-Motion-Optimizer.git
synced 2026-09-25 07:10:09 +03:00
closeEvent eklendi. Önceden pencere optimizasyon sırasında kapatılınca çalışan
QThread yok ediliyor ve Qt abort() çağırıyordu; --windowed derlemede konsol
olmadığı için kullanıcı sadece pencerenin kaybolduğunu görüyordu.
Start düğmesi çalışma boyunca devre dışı. Önceden ikinci tıklama self.worker'ı
yeniden atayıp çalışan thread'in son referansını düşürüyor, aynı abort()'a yol
açıyor ve iki thread aynı çıktıya yazıyordu.
İptal düğmesi eklendi: isInterruptionRequested, should_cancel üzerinden
optimize_vmd'ye geçiriliyor ve kanal döngülerinde kontrol ediliyor.
Profiller doğrulanıyor ve kalıcı. İçe alınan JSON hiç doğrulanmıyordu;
{"P": "aggressive"} gibi bir dosya slot içinde AttributeError'a, oradan
qFatal()'a düşüyordu. Ayrıca profiller yalnızca bellekteydi - "Profili Kaydet"
uygulama kapanınca kayboluyordu. Artık QStandardPaths.AppDataLocation altında
saklanıyor, içe alma yerleşik profilleri silmek yerine üzerine ekliyor ve
14 ayarın tamamı kaydediliyor (önceden yalnızca 4'ü).
Hata raporu: tür adı + traceback. Önceden yalnızca str(e) gönderiliyordu;
MemoryError'da hata kutusu bomboş çıkıyordu. sys.excepthook da eklendi.
Çıktı == girdi kontrolü, üzerine yazma onayı, eksik uzantı tamamlama,
sürükle-bırak, son klasörü hatırlama, %50'de donmayan ilerleme çubuğu ve
sınırlandırılmış log tamponu.
585 lines
24 KiB
Python
585 lines
24 KiB
Python
# VMD Motion Optimizer by Barış Keser (barkeser2002)
|
||
# License: GNU General Public License v3.0 (GPL-3.0)
|
||
# See LICENSE for details.
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import traceback
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
from PyQt6 import QtCore, QtWidgets, QtGui
|
||
|
||
from optimize_vmd import optimize_vmd, CancelledError
|
||
|
||
|
||
DEFAULT_PROFILES = {
|
||
"Quality": {"pos_eps": 0.02, "rot_eps_deg": 0.25, "morph_eps": 0.0005, "key_step": 1},
|
||
"Balanced": {"pos_eps": 0.05, "rot_eps_deg": 0.5, "morph_eps": 0.001, "key_step": 1},
|
||
"Aggressive": {"pos_eps": 0.1, "rot_eps_deg": 1.0, "morph_eps": 0.002, "key_step": 2},
|
||
}
|
||
|
||
# Profillerde saklanan tüm alanlar (v1.0.4 yalnızca ilk dördünü kaydediyordu,
|
||
# yani bir "profil" bir çalıştırmayı yeniden üretemiyordu).
|
||
PROFILE_FIELDS = (
|
||
"pos_eps", "rot_eps_deg", "morph_eps", "key_step",
|
||
"depth_check", "depth_smooth", "depth_scale",
|
||
"ground_check", "ground_target", "ground_smooth", "ground_scale",
|
||
"ground_all_bones", "ground_exclude_ik", "flatten_interp",
|
||
)
|
||
|
||
|
||
def _resource_path(name: str) -> Optional[str]:
|
||
# PyInstaller (MEIPASS) ve kaynak dizinlerini dene
|
||
candidates = []
|
||
base = os.path.dirname(os.path.abspath(__file__))
|
||
candidates.append(os.path.join(base, name))
|
||
candidates.append(os.path.join(os.path.dirname(base), name))
|
||
if getattr(sys, '_MEIPASS', None):
|
||
candidates.insert(0, os.path.join(sys._MEIPASS, name)) # type: ignore[attr-defined]
|
||
for p in candidates:
|
||
if os.path.exists(p):
|
||
return p
|
||
return None
|
||
|
||
|
||
def _read_version() -> str:
|
||
for rel in ("version.txt", os.path.join("..", "version.txt")):
|
||
p = _resource_path(rel) if not os.path.isabs(rel) else rel
|
||
if p and os.path.exists(p):
|
||
try:
|
||
with open(p, 'r', encoding='utf-8') as f:
|
||
return f.read().strip()
|
||
except Exception:
|
||
pass
|
||
return "dev"
|
||
|
||
|
||
def _profiles_file() -> str:
|
||
"""Profiller kullanıcı veri klasöründe saklanır.
|
||
|
||
exe'nin yanına yazmak Program Files altında izin hatası verirdi.
|
||
"""
|
||
base = QtCore.QStandardPaths.writableLocation(
|
||
QtCore.QStandardPaths.StandardLocation.AppDataLocation)
|
||
if not base:
|
||
base = str(Path.home() / ".vmd-optimizer")
|
||
os.makedirs(base, exist_ok=True)
|
||
return os.path.join(base, "profiles.json")
|
||
|
||
|
||
def _sanitize_profiles(raw) -> dict:
|
||
"""İçe alınan JSON'u doğrula. Bozuk girdiler slot içinde çökmeye yol açıyordu."""
|
||
if not isinstance(raw, dict):
|
||
raise ValueError("Profil dosyası bir JSON nesnesi olmalı")
|
||
clean = {}
|
||
for name, body in raw.items():
|
||
if not isinstance(name, str) or not isinstance(body, dict):
|
||
continue
|
||
entry = {}
|
||
for key, value in body.items():
|
||
if key not in PROFILE_FIELDS:
|
||
continue
|
||
if isinstance(value, bool):
|
||
entry[key] = value
|
||
elif isinstance(value, (int, float)):
|
||
entry[key] = value
|
||
if entry:
|
||
clean[name] = entry
|
||
if not clean:
|
||
raise ValueError("Dosyada geçerli profil bulunamadı")
|
||
return clean
|
||
|
||
|
||
class Worker(QtCore.QThread):
|
||
progress_signal = QtCore.pyqtSignal(str, int, int)
|
||
done_signal = QtCore.pyqtSignal(str)
|
||
error_signal = QtCore.pyqtSignal(str, str)
|
||
log_signal = QtCore.pyqtSignal(str)
|
||
cancelled_signal = QtCore.pyqtSignal()
|
||
|
||
def __init__(self, params: dict, parent=None):
|
||
super().__init__(parent)
|
||
self.params = params
|
||
|
||
def run(self):
|
||
try:
|
||
out = optimize_vmd(
|
||
progress=lambda s, i, t: self.progress_signal.emit(s, i, t),
|
||
log=lambda msg: self.log_signal.emit(str(msg)),
|
||
should_cancel=self.isInterruptionRequested,
|
||
**self.params,
|
||
)
|
||
self.done_signal.emit(out)
|
||
except CancelledError:
|
||
self.cancelled_signal.emit()
|
||
except Exception as e:
|
||
# v1.0.4 yalnızca str(e) gönderiyordu; MemoryError gibi hatalarda
|
||
# kullanıcı bomboş bir hata kutusu görüyordu.
|
||
summary = f"{type(e).__name__}: {e}" if str(e) else type(e).__name__
|
||
self.error_signal.emit(summary, traceback.format_exc())
|
||
|
||
|
||
class MainWindow(QtWidgets.QWidget):
|
||
def __init__(self):
|
||
super().__init__()
|
||
ver = _read_version()
|
||
self.setWindowTitle(f"VMD Motion Optimizer by Barış Keser (barkeser2002) v{ver}")
|
||
self.resize(720, 640)
|
||
self.setAcceptDrops(True)
|
||
self._last_dir = str(Path.home())
|
||
|
||
# Üstte logo
|
||
logo_path = _resource_path('logo.png')
|
||
self.logo_label = QtWidgets.QLabel()
|
||
if logo_path:
|
||
pix = QtGui.QPixmap(logo_path)
|
||
if not pix.isNull():
|
||
scaled = pix.scaledToWidth(640, QtCore.Qt.TransformationMode.SmoothTransformation)
|
||
self.logo_label.setPixmap(scaled)
|
||
self.logo_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||
self.logo_label.setSizePolicy(QtWidgets.QSizePolicy.Policy.Ignored,
|
||
QtWidgets.QSizePolicy.Policy.Fixed)
|
||
|
||
# Inputs
|
||
self.input_edit = QtWidgets.QLineEdit()
|
||
self.in_btn = QtWidgets.QPushButton("VMD Seç...")
|
||
self.output_edit = QtWidgets.QLineEdit()
|
||
self.out_btn = QtWidgets.QPushButton("Çıktı...")
|
||
|
||
self.pos_eps = QtWidgets.QDoubleSpinBox(); self.pos_eps.setRange(0.0, 10.0); self.pos_eps.setValue(0.05); self.pos_eps.setSingleStep(0.01)
|
||
self.rot_eps = QtWidgets.QDoubleSpinBox(); self.rot_eps.setRange(0.0, 45.0); self.rot_eps.setValue(0.5); self.rot_eps.setSingleStep(0.1)
|
||
self.morph_eps = QtWidgets.QDoubleSpinBox(); self.morph_eps.setRange(0.0, 1.0); self.morph_eps.setDecimals(5); self.morph_eps.setValue(0.001); self.morph_eps.setSingleStep(0.0005)
|
||
self.key_step = QtWidgets.QSpinBox(); self.key_step.setRange(1, 10); self.key_step.setValue(1)
|
||
self.flatten_interp = QtWidgets.QCheckBox("İnterpolasyon eğrilerini düzleştir (MMD varsayılanı)")
|
||
|
||
# Depth alignment removal
|
||
self.depth_check = QtWidgets.QCheckBox("Depth (Z) sürüklenmesini kaldır")
|
||
self.depth_smooth = QtWidgets.QSpinBox(); self.depth_smooth.setRange(0, 200); self.depth_smooth.setValue(30)
|
||
self.depth_smooth.setToolTip("Eğilim penceresi. 2'den küçükse depth işlemi atlanır.")
|
||
self.depth_scale = QtWidgets.QDoubleSpinBox(); self.depth_scale.setRange(0.0, 10.0); self.depth_scale.setValue(1.0); self.depth_scale.setSingleStep(0.1)
|
||
# Ground stabilization
|
||
self.ground_check = QtWidgets.QCheckBox("Ground stabilization (Y)")
|
||
self.ground_target = QtWidgets.QDoubleSpinBox(); self.ground_target.setRange(-1000.0, 1000.0); self.ground_target.setValue(0.0)
|
||
self.ground_smooth = QtWidgets.QSpinBox(); self.ground_smooth.setRange(0, 200); self.ground_smooth.setValue(0)
|
||
self.ground_scale = QtWidgets.QDoubleSpinBox(); self.ground_scale.setRange(0.0, 10.0); self.ground_scale.setValue(1.0); self.ground_scale.setSingleStep(0.1)
|
||
self.ground_all_bones = QtWidgets.QCheckBox("Tüm kemikleri kullan (varsayılan: ayak)")
|
||
self.ground_exclude_ik = QtWidgets.QCheckBox("IK kemiklerini ölçümden hariç tut")
|
||
self.ground_exclude_ik.setChecked(True)
|
||
self.ground_exclude_ik.setToolTip(
|
||
"IK hariç bırakıldığında ölçülebilir Y verisi kalmazsa IK kemikleri "
|
||
"otomatik olarak geri alınır (log'a yazılır).")
|
||
|
||
# Profiles
|
||
self.profile_combo = QtWidgets.QComboBox()
|
||
self.save_profile_btn = QtWidgets.QPushButton("Profili Kaydet")
|
||
self.export_profiles_btn = QtWidgets.QPushButton("Dışa Aktar")
|
||
self.import_profiles_btn = QtWidgets.QPushButton("İçe Al")
|
||
|
||
self.start_btn = QtWidgets.QPushButton("Optimize Et")
|
||
self.cancel_btn = QtWidgets.QPushButton("İptal")
|
||
self.cancel_btn.setEnabled(False)
|
||
self.progress = QtWidgets.QProgressBar()
|
||
self.progress.setRange(0, 100)
|
||
self.progress.setFormat("%p% %v")
|
||
self.log = QtWidgets.QPlainTextEdit(); self.log.setReadOnly(True)
|
||
self.log.setMaximumBlockCount(5000)
|
||
|
||
# Credit etiketi
|
||
self.credit = QtWidgets.QLabel(
|
||
f"VMD Motion Optimizer by Barış Keser (barkeser2002) — GPL-3.0 — v{ver}")
|
||
self.credit.setStyleSheet("color: gray; font-size: 11px;")
|
||
|
||
form = QtWidgets.QFormLayout()
|
||
form.addRow("Girdi VMD:", self._with_btn(self.input_edit, self.in_btn))
|
||
form.addRow("Çıktı VMD:", self._with_btn(self.output_edit, self.out_btn))
|
||
form.addRow("Pozisyon eps:", self.pos_eps)
|
||
form.addRow("Rotasyon eps (deg):", self.rot_eps)
|
||
form.addRow("Morph eps:", self.morph_eps)
|
||
form.addRow("Key step:", self.key_step)
|
||
form.addRow(self.flatten_interp)
|
||
form.addRow(self.depth_check)
|
||
form.addRow("Depth eğilim penceresi:", self.depth_smooth)
|
||
form.addRow("Depth scale:", self.depth_scale)
|
||
form.addRow(self.ground_check)
|
||
form.addRow("Ground target Y:", self.ground_target)
|
||
form.addRow("Ground smooth window:", self.ground_smooth)
|
||
form.addRow("Ground scale:", self.ground_scale)
|
||
form.addRow(self.ground_all_bones)
|
||
form.addRow(self.ground_exclude_ik)
|
||
|
||
prof_row = QtWidgets.QHBoxLayout()
|
||
prof_row.addWidget(self.profile_combo, 1)
|
||
prof_row.addWidget(self.save_profile_btn)
|
||
prof_row.addWidget(self.export_profiles_btn)
|
||
prof_row.addWidget(self.import_profiles_btn)
|
||
prof_widget = QtWidgets.QWidget(); prof_widget.setLayout(prof_row)
|
||
form.addRow("Profil:", prof_widget)
|
||
|
||
btn_row = QtWidgets.QHBoxLayout()
|
||
btn_row.addWidget(self.start_btn, 3)
|
||
btn_row.addWidget(self.cancel_btn, 1)
|
||
|
||
v = QtWidgets.QVBoxLayout(self)
|
||
if logo_path:
|
||
v.addWidget(self.logo_label)
|
||
v.addLayout(form)
|
||
v.addLayout(btn_row)
|
||
v.addWidget(self.progress)
|
||
v.addWidget(self.log)
|
||
v.addWidget(self.credit)
|
||
|
||
# signals
|
||
self.in_btn.clicked.connect(self.select_input)
|
||
self.out_btn.clicked.connect(self.select_output)
|
||
self.start_btn.clicked.connect(self.start)
|
||
self.cancel_btn.clicked.connect(self.cancel)
|
||
self.save_profile_btn.clicked.connect(self.save_profile)
|
||
self.export_profiles_btn.clicked.connect(self.export_profiles)
|
||
self.import_profiles_btn.clicked.connect(self.import_profiles)
|
||
|
||
self._profiles = dict(DEFAULT_PROFILES)
|
||
self._load_profiles_from_disk()
|
||
self._refresh_profile_combo()
|
||
# Combo doldurulduktan SONRA bağla; aksi halde addItems sırasında
|
||
# apply_profile tetiklenir.
|
||
self.profile_combo.currentTextChanged.connect(self.apply_profile)
|
||
self.apply_profile(self.profile_combo.currentText())
|
||
|
||
self.worker: Optional[Worker] = None
|
||
|
||
# ---------- yardımcılar ----------
|
||
|
||
def _with_btn(self, widget, btn):
|
||
h = QtWidgets.QHBoxLayout(); w = QtWidgets.QWidget()
|
||
h.addWidget(widget); h.addWidget(btn); w.setLayout(h)
|
||
return w
|
||
|
||
def log_text(self, s: str):
|
||
self.log.appendPlainText(s)
|
||
|
||
def _warn(self, title: str, msg: str):
|
||
self.log_text(msg)
|
||
QtWidgets.QMessageBox.warning(self, title, msg)
|
||
|
||
def _is_running(self) -> bool:
|
||
return self.worker is not None and self.worker.isRunning()
|
||
|
||
# ---------- sürükle-bırak ----------
|
||
|
||
def dragEnterEvent(self, event: QtGui.QDragEnterEvent):
|
||
urls = event.mimeData().urls()
|
||
if urls and urls[0].toLocalFile().lower().endswith('.vmd'):
|
||
event.acceptProposedAction()
|
||
|
||
def dropEvent(self, event: QtGui.QDropEvent):
|
||
path = event.mimeData().urls()[0].toLocalFile()
|
||
self._set_input(path)
|
||
event.acceptProposedAction()
|
||
|
||
# ---------- dosya seçimi ----------
|
||
|
||
def _set_input(self, path: str):
|
||
self.input_edit.setText(path)
|
||
self._last_dir = os.path.dirname(path) or self._last_dir
|
||
if not self.output_edit.text():
|
||
self.output_edit.setText(os.path.splitext(path)[0] + "_optimized.vmd")
|
||
|
||
def select_input(self):
|
||
path, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||
self, "VMD seç", self._last_dir, "VMD Files (*.vmd *.VMD)")
|
||
if path:
|
||
self._set_input(path)
|
||
|
||
def select_output(self):
|
||
dlg_start = self.output_edit.text() or self._last_dir
|
||
path, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||
self, "Çıktı yolu", dlg_start, "VMD Files (*.vmd)")
|
||
if path:
|
||
if not os.path.splitext(path)[1]:
|
||
path += ".vmd"
|
||
self.output_edit.setText(path)
|
||
|
||
# ---------- profiller ----------
|
||
|
||
def _current_settings(self) -> dict:
|
||
return {
|
||
"pos_eps": self.pos_eps.value(),
|
||
"rot_eps_deg": self.rot_eps.value(),
|
||
"morph_eps": self.morph_eps.value(),
|
||
"key_step": self.key_step.value(),
|
||
"depth_check": self.depth_check.isChecked(),
|
||
"depth_smooth": self.depth_smooth.value(),
|
||
"depth_scale": self.depth_scale.value(),
|
||
"ground_check": self.ground_check.isChecked(),
|
||
"ground_target": self.ground_target.value(),
|
||
"ground_smooth": self.ground_smooth.value(),
|
||
"ground_scale": self.ground_scale.value(),
|
||
"ground_all_bones": self.ground_all_bones.isChecked(),
|
||
"ground_exclude_ik": self.ground_exclude_ik.isChecked(),
|
||
"flatten_interp": self.flatten_interp.isChecked(),
|
||
}
|
||
|
||
def _refresh_profile_combo(self, select: Optional[str] = None):
|
||
blocked = self.profile_combo.blockSignals(True)
|
||
current = select or self.profile_combo.currentText()
|
||
self.profile_combo.clear()
|
||
self.profile_combo.addItems(self._profiles.keys())
|
||
if current in self._profiles:
|
||
self.profile_combo.setCurrentText(current)
|
||
self.profile_combo.blockSignals(blocked)
|
||
|
||
def _load_profiles_from_disk(self):
|
||
path = _profiles_file()
|
||
if not os.path.exists(path):
|
||
return
|
||
try:
|
||
with open(path, 'r', encoding='utf-8') as f:
|
||
self._profiles.update(_sanitize_profiles(json.load(f)))
|
||
except Exception as e:
|
||
self.log_text(f"Kayıtlı profiller okunamadı ({type(e).__name__}: {e})")
|
||
|
||
def _save_profiles_to_disk(self):
|
||
try:
|
||
with open(_profiles_file(), 'w', encoding='utf-8') as f:
|
||
json.dump(self._profiles, f, ensure_ascii=False, indent=2)
|
||
except Exception as e:
|
||
self._warn("Profil", f"Profiller kaydedilemedi: {type(e).__name__}: {e}")
|
||
|
||
def apply_profile(self, name: str):
|
||
p = self._profiles.get(name)
|
||
if not isinstance(p, dict):
|
||
return
|
||
self.pos_eps.setValue(float(p.get("pos_eps", self.pos_eps.value())))
|
||
self.rot_eps.setValue(float(p.get("rot_eps_deg", self.rot_eps.value())))
|
||
self.morph_eps.setValue(float(p.get("morph_eps", self.morph_eps.value())))
|
||
self.key_step.setValue(int(p.get("key_step", self.key_step.value())))
|
||
self.depth_check.setChecked(bool(p.get("depth_check", self.depth_check.isChecked())))
|
||
self.depth_smooth.setValue(int(p.get("depth_smooth", self.depth_smooth.value())))
|
||
self.depth_scale.setValue(float(p.get("depth_scale", self.depth_scale.value())))
|
||
self.ground_check.setChecked(bool(p.get("ground_check", self.ground_check.isChecked())))
|
||
self.ground_target.setValue(float(p.get("ground_target", self.ground_target.value())))
|
||
self.ground_smooth.setValue(int(p.get("ground_smooth", self.ground_smooth.value())))
|
||
self.ground_scale.setValue(float(p.get("ground_scale", self.ground_scale.value())))
|
||
self.ground_all_bones.setChecked(bool(p.get("ground_all_bones", self.ground_all_bones.isChecked())))
|
||
self.ground_exclude_ik.setChecked(bool(p.get("ground_exclude_ik", self.ground_exclude_ik.isChecked())))
|
||
self.flatten_interp.setChecked(bool(p.get("flatten_interp", self.flatten_interp.isChecked())))
|
||
|
||
def save_profile(self):
|
||
name, ok = QtWidgets.QInputDialog.getText(self, "Profil adı", "Ad")
|
||
name = (name or '').strip()
|
||
if not ok or not name:
|
||
return
|
||
if name in self._profiles:
|
||
confirm = QtWidgets.QMessageBox.question(
|
||
self, "Profil", f"'{name}' profili zaten var. Üzerine yazılsın mı?")
|
||
if confirm != QtWidgets.QMessageBox.StandardButton.Yes:
|
||
return
|
||
self._profiles[name] = self._current_settings()
|
||
self._save_profiles_to_disk()
|
||
self._refresh_profile_combo(select=name)
|
||
self.log_text(f"Profil kaydedildi: {name}")
|
||
|
||
def export_profiles(self):
|
||
path, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||
self, "Profilleri dışa aktar",
|
||
str(Path(self._last_dir) / "profiles.json"), "JSON (*.json)")
|
||
if not path:
|
||
return
|
||
try:
|
||
with open(path, 'w', encoding='utf-8') as f:
|
||
json.dump(self._profiles, f, ensure_ascii=False, indent=2)
|
||
self.log_text("Profiller kaydedildi: " + path)
|
||
except Exception as e:
|
||
self._warn("Profil", f"Dışa aktarılamadı: {type(e).__name__}: {e}")
|
||
|
||
def import_profiles(self):
|
||
path, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||
self, "Profilleri içe al", self._last_dir, "JSON (*.json)")
|
||
if not path:
|
||
return
|
||
try:
|
||
with open(path, 'r', encoding='utf-8') as f:
|
||
incoming = _sanitize_profiles(json.load(f))
|
||
except Exception as e:
|
||
self._warn("Profil", f"İçe alınamadı: {type(e).__name__}: {e}")
|
||
return
|
||
# Yerleşik profilleri silme, üzerine ekle
|
||
self._profiles.update(incoming)
|
||
self._save_profiles_to_disk()
|
||
self._refresh_profile_combo()
|
||
self.log_text(f"{len(incoming)} profil yüklendi: {path}")
|
||
|
||
# ---------- çalıştırma ----------
|
||
|
||
def start(self):
|
||
if self._is_running():
|
||
return
|
||
inp = self.input_edit.text().strip()
|
||
outp = self.output_edit.text().strip()
|
||
if not inp or not os.path.isfile(inp):
|
||
self._warn("Girdi", "Geçerli bir VMD dosyası seçin.")
|
||
return
|
||
if not outp:
|
||
outp = os.path.splitext(inp)[0] + "_optimized.vmd"
|
||
self.output_edit.setText(outp)
|
||
if os.path.exists(outp) and os.path.realpath(inp) == os.path.realpath(outp):
|
||
self._warn("Çıktı",
|
||
"Çıktı yolu girdi ile aynı. Kaynak dosyanın üzerine yazılmaması "
|
||
"için farklı bir çıktı seçin.")
|
||
return
|
||
if os.path.exists(outp):
|
||
confirm = QtWidgets.QMessageBox.question(
|
||
self, "Çıktı", f"'{os.path.basename(outp)}' zaten var. Üzerine yazılsın mı?")
|
||
if confirm != QtWidgets.QMessageBox.StandardButton.Yes:
|
||
return
|
||
|
||
params = dict(
|
||
input_path=inp,
|
||
output_path=outp,
|
||
pos_eps=self.pos_eps.value(),
|
||
rot_eps_deg=self.rot_eps.value(),
|
||
morph_eps=self.morph_eps.value(),
|
||
key_step=self.key_step.value(),
|
||
preserve_end_keys=True,
|
||
flatten_interp=self.flatten_interp.isChecked(),
|
||
remove_depth=self.depth_check.isChecked(),
|
||
depth_smooth_window=self.depth_smooth.value(),
|
||
depth_scale=self.depth_scale.value(),
|
||
stabilize_ground_flag=self.ground_check.isChecked(),
|
||
ground_target_y=self.ground_target.value(),
|
||
ground_use_feet_only=not self.ground_all_bones.isChecked(),
|
||
ground_smooth_window=self.ground_smooth.value(),
|
||
ground_scale=self.ground_scale.value(),
|
||
ground_exclude_ik=self.ground_exclude_ik.isChecked(),
|
||
)
|
||
|
||
self.progress.setValue(0)
|
||
self.log_text("Başladı...")
|
||
self._set_running(True)
|
||
|
||
self.worker = Worker(params, parent=self)
|
||
self.worker.progress_signal.connect(self.on_progress)
|
||
self.worker.done_signal.connect(self.on_done)
|
||
self.worker.error_signal.connect(self.on_error)
|
||
self.worker.log_signal.connect(self.on_log)
|
||
self.worker.cancelled_signal.connect(self.on_cancelled)
|
||
self.worker.start()
|
||
|
||
def cancel(self):
|
||
if self._is_running():
|
||
self.log_text("İptal isteniyor...")
|
||
self.cancel_btn.setEnabled(False)
|
||
self.worker.requestInterruption()
|
||
|
||
def _set_running(self, running: bool):
|
||
self.start_btn.setEnabled(not running)
|
||
self.cancel_btn.setEnabled(running)
|
||
for w in (self.in_btn, self.out_btn, self.input_edit, self.output_edit,
|
||
self.save_profile_btn, self.import_profiles_btn, self.profile_combo):
|
||
w.setEnabled(not running)
|
||
|
||
def _finish(self):
|
||
self._set_running(False)
|
||
if self.worker is not None:
|
||
self.worker.deleteLater()
|
||
self.worker = None
|
||
|
||
@QtCore.pyqtSlot(str)
|
||
def on_log(self, s: str):
|
||
self.log_text(s)
|
||
|
||
@QtCore.pyqtSlot(str, int, int)
|
||
def on_progress(self, section: str, i: int, total: int):
|
||
# Bones %0-70, Morphs %70-100
|
||
frac = i / max(total, 1)
|
||
pct = int(frac * 70) if section == 'Bones' else 70 + int(frac * 30)
|
||
self.progress.setValue(max(0, min(100, pct)))
|
||
self.progress.setFormat(f"{section} %p%")
|
||
|
||
@QtCore.pyqtSlot(str)
|
||
def on_done(self, out: str):
|
||
self.progress.setValue(100)
|
||
self.progress.setFormat("Tamamlandı %p%")
|
||
self.log_text("Bitti. Kaydedildi: " + out)
|
||
self._finish()
|
||
|
||
@QtCore.pyqtSlot()
|
||
def on_cancelled(self):
|
||
self.progress.setValue(0)
|
||
self.progress.setFormat("İptal edildi")
|
||
self.log_text("İşlem iptal edildi; çıktı dosyası yazılmadı.")
|
||
self._finish()
|
||
|
||
@QtCore.pyqtSlot(str, str)
|
||
def on_error(self, summary: str, detail: str):
|
||
self.progress.setValue(0)
|
||
self.progress.setFormat("Hata")
|
||
self.log_text("Hata: " + summary)
|
||
box = QtWidgets.QMessageBox(self)
|
||
box.setIcon(QtWidgets.QMessageBox.Icon.Critical)
|
||
box.setWindowTitle("Hata")
|
||
box.setText(summary)
|
||
box.setDetailedText(detail)
|
||
box.exec()
|
||
self._finish()
|
||
|
||
# ---------- kapanış ----------
|
||
|
||
def closeEvent(self, event: QtGui.QCloseEvent):
|
||
"""Çalışan bir QThread yok edilirse Qt abort() çağırır (sessiz çökme)."""
|
||
if not self._is_running():
|
||
event.accept()
|
||
return
|
||
confirm = QtWidgets.QMessageBox.question(
|
||
self, "Çıkış", "Optimizasyon sürüyor. İptal edilip çıkılsın mı?")
|
||
if confirm != QtWidgets.QMessageBox.StandardButton.Yes:
|
||
event.ignore()
|
||
return
|
||
self.worker.requestInterruption()
|
||
if not self.worker.wait(5000):
|
||
self.worker.terminate()
|
||
self.worker.wait(2000)
|
||
event.accept()
|
||
|
||
|
||
def _install_excepthook():
|
||
"""--windowed derlemede konsol yoktur; yakalanmayan hata sessizce öldürür."""
|
||
def hook(exc_type, exc, tb):
|
||
text = ''.join(traceback.format_exception(exc_type, exc, tb))
|
||
try:
|
||
sys.stderr.write(text)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
box = QtWidgets.QMessageBox()
|
||
box.setIcon(QtWidgets.QMessageBox.Icon.Critical)
|
||
box.setWindowTitle("Beklenmeyen hata")
|
||
box.setText(f"{exc_type.__name__}: {exc}")
|
||
box.setDetailedText(text)
|
||
box.exec()
|
||
except Exception:
|
||
pass
|
||
sys.excepthook = hook
|
||
|
||
|
||
def main():
|
||
app = QtWidgets.QApplication(sys.argv)
|
||
app.setApplicationName("VMDOptimizer")
|
||
app.setOrganizationName("barkeser2002")
|
||
_install_excepthook()
|
||
# Uygulama ikonu: önce icon.ico, yoksa logo.png
|
||
icon_path = _resource_path('icon.ico') or _resource_path('logo.png')
|
||
if icon_path:
|
||
app.setWindowIcon(QtGui.QIcon(icon_path))
|
||
w = MainWindow()
|
||
if icon_path:
|
||
w.setWindowIcon(QtGui.QIcon(icon_path))
|
||
w.show()
|
||
sys.exit(app.exec())
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|