- 新增 _calculate_lane_conflicts_v2() 分開返回標籤重疊和線穿框分數 - 修改泳道選擇算法,優先選擇無標籤重疊的泳道 - 兩階段搜尋:優先側別無可用泳道則嘗試另一側 - 增強日誌輸出,顯示標籤範圍和詳細衝突分數 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
158 lines
4.0 KiB
Python
158 lines
4.0 KiB
Python
"""
|
||
TimeLine Designer - PyWebview 主程式
|
||
|
||
本程式整合 FastAPI 後端與 HTML 前端,提供桌面應用介面。
|
||
|
||
Author: AI Agent
|
||
Version: 1.0.0
|
||
DocID: SDD-APP-001
|
||
Rationale: 實現 SDD.md 定義的 PyWebview Host 架構
|
||
"""
|
||
|
||
import webview
|
||
import threading
|
||
import uvicorn
|
||
import logging
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# 設定日誌
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class TimelineDesignerApp:
|
||
"""
|
||
TimeLine Designer 應用程式類別
|
||
|
||
負責啟動 FastAPI 後端與 PyWebview 前端。
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.api_port = 8000
|
||
self.api_host = "127.0.0.1"
|
||
self.frontend_path = self._get_frontend_path()
|
||
|
||
def _get_frontend_path(self) -> str:
|
||
"""
|
||
取得前端 HTML 檔案路徑
|
||
|
||
Returns:
|
||
前端 index.html 的絕對路徑
|
||
"""
|
||
# 開發模式:從專案目錄載入
|
||
dev_path = Path(__file__).parent / "frontend" / "static" / "index.html"
|
||
if dev_path.exists():
|
||
return str(dev_path.absolute())
|
||
|
||
# 打包模式:從執行檔旁邊載入
|
||
bundle_path = Path(sys.executable).parent / "frontend" / "static" / "index.html"
|
||
if bundle_path.exists():
|
||
return str(bundle_path.absolute())
|
||
|
||
# 找不到前端檔案
|
||
logger.error("找不到前端 HTML 檔案")
|
||
raise FileNotFoundError("Frontend index.html not found")
|
||
|
||
def start_api_server(self):
|
||
"""
|
||
啟動 FastAPI 後端伺服器
|
||
|
||
在獨立執行緒中運行,避免阻塞主程式。
|
||
"""
|
||
try:
|
||
from backend.main import app
|
||
|
||
logger.info(f"正在啟動 API 伺服器於 http://{self.api_host}:{self.api_port}")
|
||
|
||
# 設定 uvicorn
|
||
config = uvicorn.Config(
|
||
app,
|
||
host=self.api_host,
|
||
port=self.api_port,
|
||
log_level="info"
|
||
)
|
||
server = uvicorn.Server(config)
|
||
server.run()
|
||
|
||
except Exception as e:
|
||
logger.error(f"API 伺服器啟動失敗: {str(e)}")
|
||
raise
|
||
|
||
def start_gui(self):
|
||
"""
|
||
啟動 PyWebview GUI
|
||
|
||
在主執行緒中運行。
|
||
"""
|
||
try:
|
||
logger.info("正在啟動 GUI 視窗")
|
||
|
||
# 建立視窗
|
||
window = webview.create_window(
|
||
title='TimeLine Designer',
|
||
url=self.frontend_path,
|
||
width=1400,
|
||
height=900,
|
||
resizable=True,
|
||
fullscreen=False,
|
||
min_size=(1024, 768),
|
||
)
|
||
|
||
logger.info("GUI 視窗已建立")
|
||
|
||
# 啟動 webview(這會阻塞直到視窗關閉)
|
||
webview.start(debug=True)
|
||
|
||
logger.info("GUI 視窗已關閉")
|
||
|
||
except Exception as e:
|
||
logger.error(f"GUI 啟動失敗: {str(e)}")
|
||
raise
|
||
|
||
def run(self):
|
||
"""
|
||
執行應用程式
|
||
|
||
啟動順序:
|
||
1. 在背景執行緒啟動 FastAPI 伺服器
|
||
2. 在主執行緒啟動 PyWebview GUI
|
||
"""
|
||
logger.info("=== TimeLine Designer 啟動中 ===")
|
||
|
||
# 在背景執行緒啟動 API 伺服器
|
||
api_thread = threading.Thread(target=self.start_api_server, daemon=True)
|
||
api_thread.start()
|
||
|
||
# 等待 API 伺服器啟動
|
||
import time
|
||
logger.info("等待 API 伺服器啟動...")
|
||
time.sleep(2)
|
||
|
||
# 在主執行緒啟動 GUI
|
||
self.start_gui()
|
||
|
||
logger.info("=== TimeLine Designer 已關閉 ===")
|
||
|
||
|
||
def main():
|
||
"""
|
||
應用程式入口點
|
||
"""
|
||
try:
|
||
app = TimelineDesignerApp()
|
||
app.run()
|
||
except KeyboardInterrupt:
|
||
logger.info("使用者中斷程式")
|
||
sys.exit(0)
|
||
except Exception as e:
|
||
logger.error(f"應用程式錯誤: {str(e)}")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|