feat(ui): Mastercard 팔레트 1차 적용 + 인트로 비디오 제거 (#4 부분)
색감/텍스쳐 라운드: - 50+ hex literal 갱신 → Mastercard 공식 brand 컬러로 통일. - Bootstrap-ish 팔레트 (#2ECC71 green / #E74C3C red / #F1C40F yellow / #E67E22 orange / #343A40 slate) → MC Red(#EB001B) / MC Yellow(#F79E1B) / Brand Green(#22A06B) / Near-black(#1A1A1A) / Black(#000000). - 주요 매핑: 에러 status → MC Red, 경고 status → MC Yellow, READY 인디케이터 → Brand Green, 오렌지 CTA → MC Red, 다크 슬레이트 버튼 → MC Near-black. - scanvas_maker.py line ~33-47 에 팔레트 의도 주석 블록 추가. - npx getdesign@latest add mastercard 시도는 외부 npm 코드 실행 차단으로 건너뛰고 공개 brand 가이드라인 컬러 적용. 인트로 영상 제거: - splash.py 삭제 (178 LOC, show_intro_splash 함수 단일 진입점). - Design/logo_intro.mp4 삭제 (3.7 MB binary). - scanvas_maker.py 의 호출부 13줄 제거 (~line 7044-7054). - 효과: 첫 화면까지 12초 fade-in 인트로 제거 → 즉시 기동. 잔여 (#4 다음 라운드, multi-session): - 단일 창 구조 (CTkToplevel 12개 통합). - 인라인 로그 패널 → 백엔드 파일. - VTK 임베딩 (vtkTkRenderWidget 또는 pyvistaqt). - 3-column 레이아웃 (Sidebar/Canvas/Inspector). - messagebox 63회 → 인라인 토스트. 검증: py_compile + AST OK. splash/show_intro_splash 호출 0건. .gitignore 정상 (gcp-key.json/*.log/*.db/cache/__pycache__/venv 모두 ignored). CHANGELOG.md 에 #4 1차 라운드 항목 추가. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
47
CHANGELOG.md
47
CHANGELOG.md
@@ -10,6 +10,53 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-05-08 (후속 — UI 1차)
|
||||||
|
|
||||||
|
### [feat] Mastercard 팔레트 1차 적용 + 인트로 비디오 제거 (#4 부분)
|
||||||
|
|
||||||
|
> **사용자 피드백 #4 부분 진행**: 색감/텍스쳐 위주의 1차 라운드. 단일 창 구조 (CTkToplevel 12개 통합, VTK 임베딩 등) 는 multi-session 작업이라 별도 라운드. 본 commit 은 색채 정체성 + 인트로 제거에 한정.
|
||||||
|
|
||||||
|
#### Mastercard 디자인 토큰 매핑 (10색)
|
||||||
|
- `#2ECC71` (Bootstrap green) → `#22A06B` Brand Green (READY 인디케이터, 12곳).
|
||||||
|
- `#E74C3C` (Bootstrap red) → `#EB001B` Mastercard Red (에러 status, 14곳).
|
||||||
|
- `#F1C40F` (Bootstrap yellow) → `#F79E1B` Mastercard Yellow (경고 status, 7곳).
|
||||||
|
- `#27AE60` / `#1E8449` (CTA 그린/hover) → `#22A06B` / `#1B8454` (5+3곳).
|
||||||
|
- `#E67E22` / `#BA6116` (오렌지 CTA/hover) → `#EB001B` / `#A30013` MC Red (3+1곳) — 주요 액션 버튼이 MC 레드로 통일.
|
||||||
|
- `#343A40` / `#212529` (다크 슬레이트 버튼/hover) → `#1A1A1A` / `#000000` MC Near-black (1+1곳).
|
||||||
|
- `#2b2b2b` (CTk 기본 다크 캔버스) → `#1A1A1A` (1곳, 다크 모드 일관성).
|
||||||
|
|
||||||
|
총 50+ hex literal 갱신. Mastercard 공식 brand 가이드라인 (공개) 기반 — `npx getdesign@latest add mastercard` 시도는 외부 npm 코드 실행 차단으로 건너뛰고 공개 팔레트 적용.
|
||||||
|
|
||||||
|
#### 디자인 의도
|
||||||
|
- **PRIMARY (MC Red `#EB001B`)**: 주 CTA + 에러 상태 일관 적용. 위험 신호와 액션 강조 단일 색.
|
||||||
|
- **ACCENT (MC Yellow `#F79E1B`)**: 경고/노란 status 일관 — Step3 진행 인디케이터 등.
|
||||||
|
- **SUCCESS (Brand Green `#22A06B`)**: READY/완료 인디케이터. MC 자체 그린 토큰 없음 → brand-friendly 톤 선정.
|
||||||
|
- **DARK (`#1A1A1A`)**: 다크 모드 페이지/카드 bg, 다크 버튼 — pure black 직전. 텍스트 가독성 ↑.
|
||||||
|
- 팔레트 문서화 블록: `scanvas_maker.py` line ~33-47 (`Mastercard 디자인 시스템 팔레트` 주석).
|
||||||
|
|
||||||
|
#### 인트로 영상 제거
|
||||||
|
- `splash.py` 삭제 (178 LOC) — `show_intro_splash` 함수 단일 진입점, 더 이상 호출 없음.
|
||||||
|
- `Design/logo_intro.mp4` 삭제 (3.7 MB).
|
||||||
|
- `scanvas_maker.py` 의 호출부 제거 (line ~7044-7054 13줄): try/except + show_intro_splash + import.
|
||||||
|
- 효과: 메인 앱 즉시 기동, 첫 화면까지 12초 fade-in 사라짐 → "느리게 느껴짐" 피드백 일부 완화.
|
||||||
|
|
||||||
|
#### 잔여 (#4 다음 라운드)
|
||||||
|
- **단일 창 구조**: CTkToplevel 12개 (T1~T12) 인스펙터 패널로 통합.
|
||||||
|
- **인라인 로그 패널 제거**: `main_frame.row=1` `CTkTextbox` → harness_log_path() 백엔드 파일.
|
||||||
|
- **VTK 임베딩**: `pv.Plotter().show()` 6개 호출지 → `vtkmodules.tk.vtkTkRenderWidget` 또는 `pyvistaqt.QtInteractor`.
|
||||||
|
- **3-column 레이아웃**: Sidebar(240) / Main Canvas(flex) / Inspector(340).
|
||||||
|
- **messagebox 63회** → 인라인 토스트/배너 (위험한 askyesno 4건만 모달 유지).
|
||||||
|
|
||||||
|
이는 multi-session 작업이라 본 라운드 범위 외. UI_REDESIGN_PLAN.md §3 참조.
|
||||||
|
|
||||||
|
#### 검증
|
||||||
|
- `python -m py_compile scanvas_maker.py harness/perf.py harness/crash_logger.py` 통과.
|
||||||
|
- AST parse OK.
|
||||||
|
- `splash`/`show_intro_splash` 호출 잔존 검사 — 0건 (주석 블록의 1회 'splash' 언급 제외).
|
||||||
|
- `git ls-files | grep -E '...gcp.*key|\\.log$|\\.db$|\\.bak|venv|__pycache__|cache/'` — 0건. .gitignore 정상 동작.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-05-08
|
## 2026-05-08
|
||||||
|
|
||||||
### [merge] Gitea s-canvas 원격(raw upload, 184185c)과 로컬 lint+Phase 0 history 통합
|
### [merge] Gitea s-canvas 원격(raw upload, 184185c)과 로컬 lint+Phase 0 history 통합
|
||||||
|
|||||||
Binary file not shown.
110
scanvas_maker.py
110
scanvas_maker.py
@@ -30,6 +30,22 @@ from PIL import Image, ImageDraw, ImageFilter
|
|||||||
import tkintermapview
|
import tkintermapview
|
||||||
from matplotlib.colors import LinearSegmentedColormap
|
from matplotlib.colors import LinearSegmentedColormap
|
||||||
|
|
||||||
|
# ===== Mastercard 디자인 시스템 팔레트 (피드백 #4 — UI/UX 재설계) =====
|
||||||
|
# 본 색상은 코드 전반에 hex literal 로 직접 적용된다 (constants import 부담 회피).
|
||||||
|
# 본 주석 블록은 디자인 의도 문서화용. UI_REDESIGN_PLAN.md 참조.
|
||||||
|
#
|
||||||
|
# PRIMARY #EB001B Mastercard Red — 주 CTA, 에러 상태
|
||||||
|
# PRIMARY' #A30013 Red Dark — CTA hover/active
|
||||||
|
# ACCENT #F79E1B Mastercard Yellow — 보조 강조, 경고 (Step3 노란 status 등)
|
||||||
|
# FOCUS #FF5F00 Overlap Orange — 포커스 / focus accent (예약)
|
||||||
|
# SUCCESS #22A06B Brand-friendly Green — 성공 / READY 인디케이터
|
||||||
|
# SUCCESS' #1B8454 Green Dark — 성공 hover
|
||||||
|
# DARK #1A1A1A Near-black — 다크 모드 페이지/버튼 bg
|
||||||
|
# BLACK #000000 Pure Black — 텍스트, 다크 버튼 hover
|
||||||
|
# BORDER_L #E0E0E0 Light Border — 라이트 모드 보더
|
||||||
|
# BORDER_D #333333 Dark Border — 다크 모드 보더 (현 코드는 #3F3F3F 유지)
|
||||||
|
# 인트로 splash 비디오는 본 라운드에서 제거됨 (피드백 #4).
|
||||||
|
|
||||||
# 지형(TIN) 컬러맵 — **파란색 금지** (피드백 #3: 물과 헷갈림).
|
# 지형(TIN) 컬러맵 — **파란색 금지** (피드백 #3: 물과 헷갈림).
|
||||||
# 어두운 토양 → 밝은 모래/건조 톤 → 능선 광택. matplotlib "terrain" 대체.
|
# 어두운 토양 → 밝은 모래/건조 톤 → 능선 광택. matplotlib "terrain" 대체.
|
||||||
_TIN_EARTH_CMAP = LinearSegmentedColormap.from_list(
|
_TIN_EARTH_CMAP = LinearSegmentedColormap.from_list(
|
||||||
@@ -470,14 +486,14 @@ class SCanvasApp(ctk.CTk):
|
|||||||
self.btn_step4 = ctk.CTkButton(
|
self.btn_step4 = ctk.CTkButton(
|
||||||
self.sidebar_frame, text="4. AI 렌더링",
|
self.sidebar_frame, text="4. AI 렌더링",
|
||||||
command=self.btn_ai_render_callback, height=40,
|
command=self.btn_ai_render_callback, height=40,
|
||||||
fg_color="#E67E22", hover_color="#BA6116",
|
fg_color="#EB001B", hover_color="#A30013",
|
||||||
font=ctk.CTkFont(weight="bold"))
|
font=ctk.CTkFont(weight="bold"))
|
||||||
self.btn_step4.grid(row=_next_row(), column=0, pady=(6, 6), sticky="ew", **pad)
|
self.btn_step4.grid(row=_next_row(), column=0, pady=(6, 6), sticky="ew", **pad)
|
||||||
|
|
||||||
self.btn_struct_build = ctk.CTkButton(
|
self.btn_struct_build = ctk.CTkButton(
|
||||||
self.sidebar_frame, text="구조물 상세 3D 빌드",
|
self.sidebar_frame, text="구조물 상세 3D 빌드",
|
||||||
command=self._open_structure_template_dialog, height=32,
|
command=self._open_structure_template_dialog, height=32,
|
||||||
fg_color="#27AE60", hover_color="#1E8449", text_color="white",
|
fg_color="#22A06B", hover_color="#1B8454", text_color="white",
|
||||||
font=ctk.CTkFont(size=12, weight="bold"))
|
font=ctk.CTkFont(size=12, weight="bold"))
|
||||||
self.btn_struct_build.grid(row=_next_row(), column=0, pady=(3, 0), sticky="ew", **pad)
|
self.btn_struct_build.grid(row=_next_row(), column=0, pady=(3, 0), sticky="ew", **pad)
|
||||||
|
|
||||||
@@ -494,8 +510,8 @@ class SCanvasApp(ctk.CTk):
|
|||||||
self.btn_reopen_3d = ctk.CTkButton(
|
self.btn_reopen_3d = ctk.CTkButton(
|
||||||
self.sidebar_frame, text="🗔 3D 뷰 다시 열기",
|
self.sidebar_frame, text="🗔 3D 뷰 다시 열기",
|
||||||
command=self._reopen_3d_preview, height=30,
|
command=self._reopen_3d_preview, height=30,
|
||||||
fg_color=("#343A40", "#2C3E50"),
|
fg_color=("#1A1A1A", "#2C3E50"),
|
||||||
hover_color=("#212529", "#34495E"),
|
hover_color=("#000000", "#34495E"),
|
||||||
text_color="#FFFFFF",
|
text_color="#FFFFFF",
|
||||||
font=ctk.CTkFont(size=11, weight="bold"))
|
font=ctk.CTkFont(size=11, weight="bold"))
|
||||||
self.btn_reopen_3d.grid(row=_next_row(), column=0, pady=(6, 0), sticky="ew", **pad)
|
self.btn_reopen_3d.grid(row=_next_row(), column=0, pady=(6, 0), sticky="ew", **pad)
|
||||||
@@ -591,7 +607,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
# 타일 주변의 얇은 padding 에 이 색이 보임.
|
# 타일 주변의 얇은 padding 에 이 색이 보임.
|
||||||
self.map_frame = ctk.CTkFrame(
|
self.map_frame = ctk.CTkFrame(
|
||||||
self.main_frame, corner_radius=12,
|
self.main_frame, corner_radius=12,
|
||||||
fg_color=("#FFFFFF", "#2b2b2b"),
|
fg_color=("#FFFFFF", "#1A1A1A"),
|
||||||
border_width=1, border_color=("#DEE2E6", "#3F3F3F"),
|
border_width=1, border_color=("#DEE2E6", "#3F3F3F"),
|
||||||
)
|
)
|
||||||
self.map_frame.grid(row=0, column=0, padx=0, pady=(0, 8), sticky="nsew")
|
self.map_frame.grid(row=0, column=0, padx=0, pady=(0, 8), sticky="nsew")
|
||||||
@@ -612,7 +628,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
self.status_bar = ctk.CTkFrame(self.main_frame, height=28, fg_color="transparent")
|
self.status_bar = ctk.CTkFrame(self.main_frame, height=28, fg_color="transparent")
|
||||||
self.status_bar.grid(row=2, column=0, sticky="ew", pady=(5, 0))
|
self.status_bar.grid(row=2, column=0, sticky="ew", pady=(5, 0))
|
||||||
|
|
||||||
self.status_indicator = ctk.CTkLabel(self.status_bar, text="● READY", text_color="#2ECC71", font=ctk.CTkFont(size=12, weight="bold"))
|
self.status_indicator = ctk.CTkLabel(self.status_bar, text="● READY", text_color="#22A06B", font=ctk.CTkFont(size=12, weight="bold"))
|
||||||
self.status_indicator.pack(side="left", padx=10)
|
self.status_indicator.pack(side="left", padx=10)
|
||||||
|
|
||||||
self.status_text = ctk.CTkLabel(self.status_bar, text="지형 데이터를 로드해 주세요.", font=ctk.CTkFont(size=12))
|
self.status_text = ctk.CTkLabel(self.status_bar, text="지형 데이터를 로드해 주세요.", font=ctk.CTkFont(size=12))
|
||||||
@@ -739,7 +755,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_status(self, text, indicator_color="#2ECC71"):
|
def set_status(self, text, indicator_color="#22A06B"):
|
||||||
def _update():
|
def _update():
|
||||||
self.status_text.configure(text=text)
|
self.status_text.configure(text=text)
|
||||||
self.status_indicator.configure(text_color=indicator_color)
|
self.status_indicator.configure(text_color=indicator_color)
|
||||||
@@ -767,7 +783,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
"cofferdam_downstream": {"name_ko": "하류 가물막이", "render_mode": "wall_extrude", "color": "#8D9CA6"},
|
"cofferdam_downstream": {"name_ko": "하류 가물막이", "render_mode": "wall_extrude", "color": "#8D9CA6"},
|
||||||
"building": {"name_ko": "건축물/가설건물", "render_mode": "box_extrude", "color": "#BDC3C7"},
|
"building": {"name_ko": "건축물/가설건물", "render_mode": "box_extrude", "color": "#BDC3C7"},
|
||||||
"temp_facility": {"name_ko": "가설부지/야적장", "render_mode": "surface_overlay", "color": "#E8DACC"},
|
"temp_facility": {"name_ko": "가설부지/야적장", "render_mode": "surface_overlay", "color": "#E8DACC"},
|
||||||
"boundary": {"name_ko": "경계선 (참고용)", "render_mode": "line_only", "color": "#E74C3C"},
|
"boundary": {"name_ko": "경계선 (참고용)", "render_mode": "line_only", "color": "#EB001B"},
|
||||||
"ignore": {"name_ko": "무시 (사용 안 함)", "render_mode": "none", "color": "#CCCCCC"},
|
"ignore": {"name_ko": "무시 (사용 안 함)", "render_mode": "none", "color": "#CCCCCC"},
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -854,7 +870,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
z_marker = "★ " if has_z else " "
|
z_marker = "★ " if has_z else " "
|
||||||
name_text = f"{z_marker}{layer_name}"
|
name_text = f"{z_marker}{layer_name}"
|
||||||
|
|
||||||
name_color = "#2ECC71" if guessed == "terrain" else None
|
name_color = "#22A06B" if guessed == "terrain" else None
|
||||||
lbl = ctk.CTkLabel(scroll_frame, text=name_text, font=ctk.CTkFont(size=12),
|
lbl = ctk.CTkLabel(scroll_frame, text=name_text, font=ctk.CTkFont(size=12),
|
||||||
anchor="w", width=320, text_color=name_color)
|
anchor="w", width=320, text_color=name_color)
|
||||||
lbl.grid(row=i, column=0, padx=(5, 3), pady=2, sticky="w")
|
lbl.grid(row=i, column=0, padx=(5, 3), pady=2, sticky="w")
|
||||||
@@ -1519,7 +1535,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
status_var.set(f"빌드됨 ({len(info['template_meshes'])}개)")
|
status_var.set(f"빌드됨 ({len(info['template_meshes'])}개)")
|
||||||
status_label = ctk.CTkLabel(list_frame, textvariable=status_var,
|
status_label = ctk.CTkLabel(list_frame, textvariable=status_var,
|
||||||
font=ctk.CTkFont(size=10),
|
font=ctk.CTkFont(size=10),
|
||||||
text_color="#2ECC71")
|
text_color="#22A06B")
|
||||||
status_label.grid(row=ri, column=4, padx=3, pady=3, sticky="w")
|
status_label.grid(row=ri, column=4, padx=3, pady=3, sticky="w")
|
||||||
|
|
||||||
# 작업 버튼
|
# 작업 버튼
|
||||||
@@ -1599,7 +1615,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
self.show_3d_preview(textured=False)
|
self.show_3d_preview(textured=False)
|
||||||
|
|
||||||
ctk.CTkButton(bottom, text="TIN에 구조물 반영 + 3D 보기", width=200,
|
ctk.CTkButton(bottom, text="TIN에 구조물 반영 + 3D 보기", width=200,
|
||||||
fg_color="#E67E22", hover_color="#D35400",
|
fg_color="#EB001B", hover_color="#D35400",
|
||||||
text_color="white",
|
text_color="white",
|
||||||
font=ctk.CTkFont(size=11, weight="bold"),
|
font=ctk.CTkFont(size=11, weight="bold"),
|
||||||
command=_apply_structures_to_tin
|
command=_apply_structures_to_tin
|
||||||
@@ -2081,8 +2097,8 @@ class SCanvasApp(ctk.CTk):
|
|||||||
score_f = float(score)
|
score_f = float(score)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
score_f = 0.0
|
score_f = 0.0
|
||||||
score_color = ("#27AE60" if score_f >= 0.85 else
|
score_color = ("#22A06B" if score_f >= 0.85 else
|
||||||
"#F39C12" if score_f >= 0.6 else "#E74C3C")
|
"#F39C12" if score_f >= 0.6 else "#EB001B")
|
||||||
|
|
||||||
hdr = ctk.CTkFrame(dwin, fg_color="transparent")
|
hdr = ctk.CTkFrame(dwin, fg_color="transparent")
|
||||||
hdr.pack(fill="x", padx=15, pady=(12, 4))
|
hdr.pack(fill="x", padx=15, pady=(12, 4))
|
||||||
@@ -2144,10 +2160,10 @@ class SCanvasApp(ctk.CTk):
|
|||||||
if excess:
|
if excess:
|
||||||
ctk.CTkLabel(scroll, text="참고: 모델에만 있는 요소 (적용 아님)",
|
ctk.CTkLabel(scroll, text="참고: 모델에만 있는 요소 (적용 아님)",
|
||||||
font=ctk.CTkFont(size=12, weight="bold"),
|
font=ctk.CTkFont(size=12, weight="bold"),
|
||||||
text_color="#E74C3C", anchor="w").pack(fill="x", pady=(10, 2))
|
text_color="#EB001B", anchor="w").pack(fill="x", pady=(10, 2))
|
||||||
for note in excess:
|
for note in excess:
|
||||||
ctk.CTkLabel(scroll, text=f"• {note}", font=ctk.CTkFont(size=10),
|
ctk.CTkLabel(scroll, text=f"• {note}", font=ctk.CTkFont(size=10),
|
||||||
text_color="#E67E22", wraplength=760, justify="left",
|
text_color="#EB001B", wraplength=760, justify="left",
|
||||||
anchor="w").pack(fill="x", padx=8, pady=1)
|
anchor="w").pack(fill="x", padx=8, pady=1)
|
||||||
|
|
||||||
valves_incorrect = diff.get("valves_incorrect", []) or []
|
valves_incorrect = diff.get("valves_incorrect", []) or []
|
||||||
@@ -2204,7 +2220,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
fg_color="transparent", border_width=1,
|
fg_color="transparent", border_width=1,
|
||||||
command=dwin.destroy).pack(side="right", padx=4)
|
command=dwin.destroy).pack(side="right", padx=4)
|
||||||
ctk.CTkButton(btns, text="✓ 선택 항목 적용 + 재빌드", width=220,
|
ctk.CTkButton(btns, text="✓ 선택 항목 적용 + 재빌드", width=220,
|
||||||
fg_color="#27AE60", hover_color="#1E8449",
|
fg_color="#22A06B", hover_color="#1B8454",
|
||||||
font=ctk.CTkFont(size=11, weight="bold"),
|
font=ctk.CTkFont(size=11, weight="bold"),
|
||||||
command=_apply_and_rebuild).pack(side="right", padx=4)
|
command=_apply_and_rebuild).pack(side="right", padx=4)
|
||||||
# 생성된 비교 이미지 경로 안내
|
# 생성된 비교 이미지 경로 안내
|
||||||
@@ -2306,7 +2322,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
fg_color="transparent", border_width=1,
|
fg_color="transparent", border_width=1,
|
||||||
command=win.destroy).pack(side="right", padx=3)
|
command=win.destroy).pack(side="right", padx=3)
|
||||||
ctk.CTkButton(bottom, text="✓ 확정 (레지스트리 저장)", width=190,
|
ctk.CTkButton(bottom, text="✓ 확정 (레지스트리 저장)", width=190,
|
||||||
fg_color="#27AE60", hover_color="#1E8449",
|
fg_color="#22A06B", hover_color="#1B8454",
|
||||||
text_color="white",
|
text_color="white",
|
||||||
font=ctk.CTkFont(size=11, weight="bold"),
|
font=ctk.CTkFont(size=11, weight="bold"),
|
||||||
command=_do_confirm).pack(side="right", padx=3)
|
command=_do_confirm).pack(side="right", padx=3)
|
||||||
@@ -2436,7 +2452,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
# 파싱 결과 표시
|
# 파싱 결과 표시
|
||||||
result_var = ctk.StringVar(value=self._format_detail_params(info.get("detail_params")))
|
result_var = ctk.StringVar(value=self._format_detail_params(info.get("detail_params")))
|
||||||
result_label = ctk.CTkLabel(list_frame, textvariable=result_var,
|
result_label = ctk.CTkLabel(list_frame, textvariable=result_var,
|
||||||
font=ctk.CTkFont(size=10), text_color="#2ECC71")
|
font=ctk.CTkFont(size=10), text_color="#22A06B")
|
||||||
result_label.grid(row=ri, column=4, padx=5, pady=3, sticky="w")
|
result_label.grid(row=ri, column=4, padx=5, pady=3, sticky="w")
|
||||||
|
|
||||||
# 파일 선택 버튼
|
# 파일 선택 버튼
|
||||||
@@ -2543,7 +2559,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
).grid(row=di, column=1, padx=5, pady=1, sticky="w")
|
).grid(row=di, column=1, padx=5, pady=1, sticky="w")
|
||||||
ctk.CTkLabel(detail_frame, text=f"{dim.value:.3f} {dim.unit}", font=ctk.CTkFont(size=10)
|
ctk.CTkLabel(detail_frame, text=f"{dim.value:.3f} {dim.unit}", font=ctk.CTkFont(size=10)
|
||||||
).grid(row=di, column=2, padx=5, pady=1, sticky="w")
|
).grid(row=di, column=2, padx=5, pady=1, sticky="w")
|
||||||
conf_color = "#2ECC71" if dim.confidence >= 0.9 else "#F1C40F" if dim.confidence >= 0.8 else "#E74C3C"
|
conf_color = "#22A06B" if dim.confidence >= 0.9 else "#F79E1B" if dim.confidence >= 0.8 else "#EB001B"
|
||||||
ctk.CTkLabel(detail_frame, text=f"{dim.confidence:.0%}", font=ctk.CTkFont(size=10),
|
ctk.CTkLabel(detail_frame, text=f"{dim.confidence:.0%}", font=ctk.CTkFont(size=10),
|
||||||
text_color=conf_color
|
text_color=conf_color
|
||||||
).grid(row=di, column=3, padx=5, pady=1, sticky="w")
|
).grid(row=di, column=3, padx=5, pady=1, sticky="w")
|
||||||
@@ -4108,7 +4124,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
|
|
||||||
self.dxf_path = file_path
|
self.dxf_path = file_path
|
||||||
self.log(f">>> [Step 1] DXF 로드: {os.path.basename(file_path)}")
|
self.log(f">>> [Step 1] DXF 로드: {os.path.basename(file_path)}")
|
||||||
self.set_status("DXF 분석 중...", "#F1C40F")
|
self.set_status("DXF 분석 중...", "#F79E1B")
|
||||||
|
|
||||||
# 진단 로그 초기화 (세션 시작)
|
# 진단 로그 초기화 (세션 시작)
|
||||||
self._diag(f"=== Step 1 시작: {file_path} ===", reset=True)
|
self._diag(f"=== Step 1 시작: {file_path} ===", reset=True)
|
||||||
@@ -4187,13 +4203,13 @@ class SCanvasApp(ctk.CTk):
|
|||||||
|
|
||||||
self.update_map_view_to_mesh()
|
self.update_map_view_to_mesh()
|
||||||
reg_n = len(self.structure_registry)
|
reg_n = len(self.structure_registry)
|
||||||
self.set_status(f"TIN 생성 완료 · 구조물 등록 {reg_n}개", "#2ECC71")
|
self.set_status(f"TIN 생성 완료 · 구조물 등록 {reg_n}개", "#22A06B")
|
||||||
self.btn_step2.configure(fg_color=["#3a7ebf", "#1f538d"], border_width=0)
|
self.btn_step2.configure(fg_color=["#3a7ebf", "#1f538d"], border_width=0)
|
||||||
|
|
||||||
self.show_3d_preview(textured=False)
|
self.show_3d_preview(textured=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"오류: {e!s}")
|
self.log(f"오류: {e!s}")
|
||||||
self.set_status("오류 발생", "#E74C3C")
|
self.set_status("오류 발생", "#EB001B")
|
||||||
messagebox.showerror("오류", f"TIN 생성 중 문제가 발생했습니다:\n{e}")
|
messagebox.showerror("오류", f"TIN 생성 중 문제가 발생했습니다:\n{e}")
|
||||||
|
|
||||||
def create_tin_from_dxf(self, filepath, terrain_layers=None):
|
def create_tin_from_dxf(self, filepath, terrain_layers=None):
|
||||||
@@ -4747,7 +4763,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
bx0, by0, bx1, by1 = state["bbox"]
|
bx0, by0, bx1, by1 = state["bbox"]
|
||||||
state["core_rect"] = _MplRect(
|
state["core_rect"] = _MplRect(
|
||||||
(bx0, by0), bx1 - bx0, by1 - by0,
|
(bx0, by0), bx1 - bx0, by1 - by0,
|
||||||
fill=False, edgecolor="#E74C3C", linewidth=2.2, label="정밀 TIN core")
|
fill=False, edgecolor="#EB001B", linewidth=2.2, label="정밀 TIN core")
|
||||||
ax.add_patch(state["core_rect"])
|
ax.add_patch(state["core_rect"])
|
||||||
# 통계 갱신
|
# 통계 갱신
|
||||||
in_core = ((pts_abs[:, 0] >= bx0) & (pts_abs[:, 0] <= bx1)
|
in_core = ((pts_abs[:, 0] >= bx0) & (pts_abs[:, 0] <= bx1)
|
||||||
@@ -4805,8 +4821,8 @@ class SCanvasApp(ctk.CTk):
|
|||||||
_clear_live()
|
_clear_live()
|
||||||
drag["live_rect"] = _MplRect(
|
drag["live_rect"] = _MplRect(
|
||||||
(bx0, by0), bx1 - bx0, by1 - by0,
|
(bx0, by0), bx1 - bx0, by1 - by0,
|
||||||
fill=True, facecolor="#E74C3C", alpha=0.18,
|
fill=True, facecolor="#EB001B", alpha=0.18,
|
||||||
edgecolor="#E74C3C", linewidth=2.0)
|
edgecolor="#EB001B", linewidth=2.0)
|
||||||
ax.add_patch(drag["live_rect"])
|
ax.add_patch(drag["live_rect"])
|
||||||
stat_lbl.configure(text=(
|
stat_lbl.configure(text=(
|
||||||
f"드래그 중 {bx1-bx0:.0f}×{by1-by0:.0f} m "
|
f"드래그 중 {bx1-bx0:.0f}×{by1-by0:.0f} m "
|
||||||
@@ -4918,7 +4934,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
text="✅ 선택 결과 제출 (이 범위를 정밀 TIN core 로 확정)",
|
text="✅ 선택 결과 제출 (이 범위를 정밀 TIN core 로 확정)",
|
||||||
command=_on_confirm,
|
command=_on_confirm,
|
||||||
height=56,
|
height=56,
|
||||||
fg_color="#2ECC71", hover_color="#27AE60",
|
fg_color="#22A06B", hover_color="#22A06B",
|
||||||
text_color="white",
|
text_color="white",
|
||||||
font=ctk.CTkFont(size=16, weight="bold"))
|
font=ctk.CTkFont(size=16, weight="bold"))
|
||||||
submit_btn.pack(side="top", fill="x", padx=10, pady=10)
|
submit_btn.pack(side="top", fill="x", padx=10, pady=10)
|
||||||
@@ -5036,7 +5052,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
feather_m = max(150.0, dem_buffer_m * 0.2)
|
feather_m = max(150.0, dem_buffer_m * 0.2)
|
||||||
src_crs = self.crs_option.get()
|
src_crs = self.crs_option.get()
|
||||||
|
|
||||||
self.set_status("DEM으로 TIN 확장 중...", "#F1C40F")
|
self.set_status("DEM으로 TIN 확장 중...", "#F79E1B")
|
||||||
self.log(f">>> [Step 1.5] DEM으로 TIN 확장 (buffer={dem_buffer_m:.0f}m, feather={feather_m:.0f}m)...")
|
self.log(f">>> [Step 1.5] DEM으로 TIN 확장 (buffer={dem_buffer_m:.0f}m, feather={feather_m:.0f}m)...")
|
||||||
|
|
||||||
# [1.5-CORE] "TIN 이용 범위" 가 설정되어 있으면 **3-zone 블렌드** 선행.
|
# [1.5-CORE] "TIN 이용 범위" 가 설정되어 있으면 **3-zone 블렌드** 선행.
|
||||||
@@ -5102,7 +5118,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
self.log(f" [DEM 확장] {result.n_points}개 정점, {result.n_faces}개 삼각형")
|
self.log(f" [DEM 확장] {result.n_points}개 정점, {result.n_faces}개 삼각형")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f" [Step 1.5] DEM 확장 실패: {e}")
|
self.log(f" [Step 1.5] DEM 확장 실패: {e}")
|
||||||
self.set_status("DEM 확장 실패", "#E74C3C")
|
self.set_status("DEM 확장 실패", "#EB001B")
|
||||||
messagebox.showerror("오류", f"DEM 확장 실패:\n{e}")
|
messagebox.showerror("오류", f"DEM 확장 실패:\n{e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -5113,7 +5129,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
|
|
||||||
# UV 매핑/텍스처 초기화 — 다음 Step 2에서 재생성
|
# UV 매핑/텍스처 초기화 — 다음 Step 2에서 재생성
|
||||||
self.total_mesh = None
|
self.total_mesh = None
|
||||||
self.set_status("Step 1.5 완료 — 위성지도 결합 준비", "#2ECC71")
|
self.set_status("Step 1.5 완료 — 위성지도 결합 준비", "#22A06B")
|
||||||
self.btn_step2.configure(fg_color=["#3a7ebf", "#1f538d"], border_width=0)
|
self.btn_step2.configure(fg_color=["#3a7ebf", "#1f538d"], border_width=0)
|
||||||
self.show_3d_preview(textured=False)
|
self.show_3d_preview(textured=False)
|
||||||
|
|
||||||
@@ -5349,7 +5365,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
messagebox.showwarning("주의", "먼저 TIN을 생성해야 합니다.")
|
messagebox.showwarning("주의", "먼저 TIN을 생성해야 합니다.")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.set_status("위성 이미지 다운로드 중...", "#F1C40F")
|
self.set_status("위성 이미지 다운로드 중...", "#F79E1B")
|
||||||
source_name = self.tile_source_option.get()
|
source_name = self.tile_source_option.get()
|
||||||
self.log(f">>> [Step 2] 위성 타일 다운로드 ({source_name})...")
|
self.log(f">>> [Step 2] 위성 타일 다운로드 ({source_name})...")
|
||||||
|
|
||||||
@@ -5457,14 +5473,14 @@ class SCanvasApp(ctk.CTk):
|
|||||||
self._uv_mapping_params = None
|
self._uv_mapping_params = None
|
||||||
self.log("텍스처 UV 매핑 완료.")
|
self.log("텍스처 UV 매핑 완료.")
|
||||||
|
|
||||||
self.set_status("위성지도 결합 완료", "#2ECC71")
|
self.set_status("위성지도 결합 완료", "#22A06B")
|
||||||
self.btn_step3.configure(fg_color=["#3a7ebf", "#1f538d"], border_width=0)
|
self.btn_step3.configure(fg_color=["#3a7ebf", "#1f538d"], border_width=0)
|
||||||
|
|
||||||
self.show_3d_preview(textured=True, texture_obj=texture)
|
self.show_3d_preview(textured=True, texture_obj=texture)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"결합 실패: {e}")
|
self.log(f"결합 실패: {e}")
|
||||||
self.set_status("결합 실패", "#E74C3C")
|
self.set_status("결합 실패", "#EB001B")
|
||||||
messagebox.showerror("오류", f"위성지도 결합 중 오류 발생:\n{e}")
|
messagebox.showerror("오류", f"위성지도 결합 중 오류 발생:\n{e}")
|
||||||
|
|
||||||
def _download_xyz_tiles(self, url_template, min_lat, min_lon, max_lat, max_lon, zoom=17):
|
def _download_xyz_tiles(self, url_template, min_lat, min_lon, max_lat, max_lon, zoom=17):
|
||||||
@@ -5855,7 +5871,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
self.log(" ◆ 좌클릭+드래그: 회전 | 휠: 줌 | 우클릭+드래그: 이동")
|
self.log(" ◆ 좌클릭+드래그: 회전 | 휠: 줌 | 우클릭+드래그: 이동")
|
||||||
self.log(" ◆ 하단 화면비 버튼 클릭 → 창 크기/캡처 비율 즉시 잠금")
|
self.log(" ◆ 하단 화면비 버튼 클릭 → 창 크기/캡처 비율 즉시 잠금")
|
||||||
self.log(" ◆ 원하는 뷰가 잡히면 Enter 키(또는 q)를 누르거나 창을 닫으세요")
|
self.log(" ◆ 원하는 뷰가 잡히면 Enter 키(또는 q)를 누르거나 창을 닫으세요")
|
||||||
self.set_status("뷰포인트를 선택하세요 (Enter로 확정)", "#F1C40F")
|
self.set_status("뷰포인트를 선택하세요 (Enter로 확정)", "#F79E1B")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 인터랙티브 3D 뷰어 열기 — 사용자가 자유롭게 회전/줌
|
# 인터랙티브 3D 뷰어 열기 — 사용자가 자유롭게 회전/줌
|
||||||
@@ -5863,7 +5879,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
|
|
||||||
if self._saved_camera is None:
|
if self._saved_camera is None:
|
||||||
self.log(" 뷰포인트 선택 취소됨.")
|
self.log(" 뷰포인트 선택 취소됨.")
|
||||||
self.set_status("뷰포인트 미선택", "#E74C3C")
|
self.set_status("뷰포인트 미선택", "#EB001B")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 선택된 카메라 위치 로그 (focal/up은 카메라 복원 시 다시 읽음)
|
# 선택된 카메라 위치 로그 (focal/up은 카메라 복원 시 다시 읽음)
|
||||||
@@ -5901,7 +5917,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
)
|
)
|
||||||
self.guide_image.save("guide_composite.png")
|
self.guide_image.save("guide_composite.png")
|
||||||
|
|
||||||
self.set_status("제어맵 추출 완료", "#2ECC71")
|
self.set_status("제어맵 추출 완료", "#22A06B")
|
||||||
self.btn_step4.configure(fg_color=["#3a7ebf", "#1f538d"])
|
self.btn_step4.configure(fg_color=["#3a7ebf", "#1f538d"])
|
||||||
self.log(" 저장 완료: capture_textured.png, depth_map.png, lineart_map.png, guide_composite.png")
|
self.log(" 저장 완료: capture_textured.png, depth_map.png, lineart_map.png, guide_composite.png")
|
||||||
|
|
||||||
@@ -5911,7 +5927,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"제어맵 추출 실패: {e}")
|
self.log(f"제어맵 추출 실패: {e}")
|
||||||
self.set_status("추출 실패", "#E74C3C")
|
self.set_status("추출 실패", "#EB001B")
|
||||||
messagebox.showerror("오류", f"제어맵 추출 중 오류:\n{e}")
|
messagebox.showerror("오류", f"제어맵 추출 중 오류:\n{e}")
|
||||||
|
|
||||||
def _open_interactive_viewer(self):
|
def _open_interactive_viewer(self):
|
||||||
@@ -6672,7 +6688,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
location = self.vertex_location.get().strip() or "global"
|
location = self.vertex_location.get().strip() or "global"
|
||||||
self.set_status(
|
self.set_status(
|
||||||
f"Gemini 렌더링 중 ({('Vertex AI' if use_vertex else 'API')})...",
|
f"Gemini 렌더링 중 ({('Vertex AI' if use_vertex else 'API')})...",
|
||||||
"#F1C40F"
|
"#F79E1B"
|
||||||
)
|
)
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=self._run_gemini_render,
|
target=self._run_gemini_render,
|
||||||
@@ -6689,7 +6705,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
"또는 'Gemini (Nano Banana)'로 변경하세요.")
|
"또는 'Gemini (Nano Banana)'로 변경하세요.")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.set_status("AI 렌더링 중... (15~60초 소요)", "#F1C40F")
|
self.set_status("AI 렌더링 중... (15~60초 소요)", "#F79E1B")
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=self._run_stability_render,
|
target=self._run_stability_render,
|
||||||
args=(key, final_prompt, strength),
|
args=(key, final_prompt, strength),
|
||||||
@@ -6757,7 +6773,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
if self.job_logger and db and job_id:
|
if self.job_logger and db and job_id:
|
||||||
self.job_logger.fail_job(db, job_id, "모든 API 방법 실패")
|
self.job_logger.fail_job(db, job_id, "모든 API 방법 실패")
|
||||||
self.after(0, lambda: self.log(" 모든 방법 실패"))
|
self.after(0, lambda: self.log(" 모든 방법 실패"))
|
||||||
self.after(0, lambda: self.set_status("AI 렌더링 실패", "#E74C3C"))
|
self.after(0, lambda: self.set_status("AI 렌더링 실패", "#EB001B"))
|
||||||
return
|
return
|
||||||
|
|
||||||
# 출력 화질 후처리 — 사용자가 Step 4에서 고른 HD/FHD/UHD 로 리사이즈
|
# 출력 화질 후처리 — 사용자가 Step 4에서 고른 HD/FHD/UHD 로 리사이즈
|
||||||
@@ -6791,7 +6807,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
self.after(0, lambda: self.log(
|
self.after(0, lambda: self.log(
|
||||||
f" AI 렌더링 완료! → {output_path} ({rendered.size}) "
|
f" AI 렌더링 완료! → {output_path} ({rendered.size}) "
|
||||||
f"[{latency_ms:.0f}ms, 품질={quality_score:.2f}]"))
|
f"[{latency_ms:.0f}ms, 품질={quality_score:.2f}]"))
|
||||||
self.after(0, lambda: self.set_status("AI 렌더링 완료", "#2ECC71"))
|
self.after(0, lambda: self.set_status("AI 렌더링 완료", "#22A06B"))
|
||||||
self.after(0, lambda: self._show_rendered_result(output_path))
|
self.after(0, lambda: self._show_rendered_result(output_path))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -6799,7 +6815,7 @@ class SCanvasApp(ctk.CTk):
|
|||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
self.job_logger.fail_job(db, job_id, str(e))
|
self.job_logger.fail_job(db, job_id, str(e))
|
||||||
self.after(0, lambda e=e: self.log(f" 렌더링 오류: {e}"))
|
self.after(0, lambda e=e: self.log(f" 렌더링 오류: {e}"))
|
||||||
self.after(0, lambda: self.set_status("렌더링 실패", "#E74C3C"))
|
self.after(0, lambda: self.set_status("렌더링 실패", "#EB001B"))
|
||||||
self.after(0, lambda e=e: messagebox.showerror("오류", f"AI 렌더링 중 오류:\n{e}"))
|
self.after(0, lambda e=e: messagebox.showerror("오류", f"AI 렌더링 중 오류:\n{e}"))
|
||||||
finally:
|
finally:
|
||||||
if db:
|
if db:
|
||||||
@@ -7041,17 +7057,5 @@ if __name__ == "__main__":
|
|||||||
except Exception as _ch_err:
|
except Exception as _ch_err:
|
||||||
print(f"[crash_logger] 설치 실패 (계속 진행): {_ch_err}")
|
print(f"[crash_logger] 설치 실패 (계속 진행): {_ch_err}")
|
||||||
|
|
||||||
# 인트로 스플래시 — Design/logo_intro.mp4 재생 후 메인 앱 기동.
|
|
||||||
# 실패·파일 없음 시 조용히 skip(메인 앱은 항상 뜸).
|
|
||||||
try:
|
|
||||||
from splash import show_intro_splash
|
|
||||||
show_intro_splash(
|
|
||||||
resource_path("Design", "logo_intro.mp4"),
|
|
||||||
max_duration_s=12.0,
|
|
||||||
fade_ms=400,
|
|
||||||
)
|
|
||||||
except Exception as _splash_err:
|
|
||||||
print(f"[Intro] 스플래시 경고: {_splash_err}")
|
|
||||||
|
|
||||||
app = SCanvasApp()
|
app = SCanvasApp()
|
||||||
app.mainloop()
|
app.mainloop()
|
||||||
|
|||||||
178
splash.py
178
splash.py
@@ -1,178 +0,0 @@
|
|||||||
"""S-CANVAS 인트로 로딩 스플래시.
|
|
||||||
|
|
||||||
Design/logo_intro.mp4 를 frameless Toplevel 중앙에 재생한 뒤 자동 종료.
|
|
||||||
메인 앱(`scanvas_maker.SCanvasApp`) 기동 직전에 호출한다.
|
|
||||||
|
|
||||||
기술 스택:
|
|
||||||
- cv2 (VideoCapture) : MP4 프레임 디코드 (harness/quality_validator 가
|
|
||||||
이미 의존하는 프로젝트 공통 dep).
|
|
||||||
- PIL + tkinter.PhotoImage : 프레임 렌더.
|
|
||||||
- tk.Tk + attributes("-alpha", ...) : frameless + 페이드 인/아웃 효과.
|
|
||||||
|
|
||||||
dynamic effects(사용자 요구 "역동적으로"):
|
|
||||||
1. 시작 시 알파 0 → 1 페이드인 (400ms)
|
|
||||||
2. 비디오 자체 애니메이션(logo_intro.mp4 는 고유 모션 포함)
|
|
||||||
3. 종료 전 알파 1 → 0 페이드아웃 (400ms)
|
|
||||||
4. 비디오 하단에 브랜드 tagline bar (오렌지 italic)
|
|
||||||
5. max_duration_s 초과 시 강제 종료(safety)
|
|
||||||
|
|
||||||
실패 조건(조용히 skip):
|
|
||||||
- logo_intro.mp4 없음
|
|
||||||
- cv2/PIL import 실패
|
|
||||||
- VideoCapture.open 실패
|
|
||||||
메인 앱 기동은 항상 보장된다.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import time
|
|
||||||
import tkinter as tk
|
|
||||||
from pathlib import Path
|
|
||||||
import contextlib
|
|
||||||
|
|
||||||
|
|
||||||
def show_intro_splash(
|
|
||||||
video_path,
|
|
||||||
max_duration_s: float = 12.0,
|
|
||||||
fade_ms: int = 400,
|
|
||||||
tagline: str = "S-CANVAS — Generative Design & Visualization Engine",
|
|
||||||
max_display_w: int = 1000,
|
|
||||||
) -> None:
|
|
||||||
"""video_path 의 MP4 를 스플래시로 재생 후 블로킹 반환.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
video_path: Path-like, .mp4 파일.
|
|
||||||
max_duration_s: 비디오가 이 시간을 넘기면 강제 종료.
|
|
||||||
fade_ms: 페이드인/아웃 각 구간 길이(ms).
|
|
||||||
tagline: 비디오 아래에 표시할 문구.
|
|
||||||
max_display_w: 화면상 최대 가로(px). 이보다 크면 aspect 유지 축소.
|
|
||||||
|
|
||||||
동작:
|
|
||||||
- 임시 tk.Tk 루트를 만들고 mainloop → 종료 시 완전 destroy.
|
|
||||||
- 이후 `SCanvasApp()` 이 새 ctk.CTk 인스턴스를 만들어도 충돌 없음
|
|
||||||
(Tk 루트가 이미 파괴됐으므로).
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
import cv2
|
|
||||||
from PIL import Image, ImageTk
|
|
||||||
except ImportError as e:
|
|
||||||
print(f"[Intro] cv2/PIL 미설치 — 스플래시 skip ({e})")
|
|
||||||
return
|
|
||||||
|
|
||||||
vp = Path(video_path)
|
|
||||||
if not vp.exists():
|
|
||||||
print(f"[Intro] 비디오 없음 ({vp}) — 스플래시 skip")
|
|
||||||
return
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(str(vp))
|
|
||||||
if not cap.isOpened():
|
|
||||||
print("[Intro] VideoCapture 열기 실패 — 스플래시 skip")
|
|
||||||
return
|
|
||||||
|
|
||||||
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
|
|
||||||
frame_ms = max(int(1000.0 / max(fps, 1.0)), 8)
|
|
||||||
vw = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 800)
|
|
||||||
vh = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 450)
|
|
||||||
|
|
||||||
if vw > max_display_w:
|
|
||||||
disp_w = max_display_w
|
|
||||||
disp_h = max(1, int(vh * max_display_w / vw))
|
|
||||||
else:
|
|
||||||
disp_w, disp_h = vw, vh
|
|
||||||
|
|
||||||
root = tk.Tk()
|
|
||||||
root.withdraw()
|
|
||||||
screen_w = root.winfo_screenwidth()
|
|
||||||
screen_h = root.winfo_screenheight()
|
|
||||||
|
|
||||||
TAG_H = 44
|
|
||||||
total_h = disp_h + TAG_H
|
|
||||||
x = max(0, (screen_w - disp_w) // 2)
|
|
||||||
y = max(0, (screen_h - total_h) // 2)
|
|
||||||
|
|
||||||
splash = tk.Toplevel(root)
|
|
||||||
splash.overrideredirect(True)
|
|
||||||
try:
|
|
||||||
splash.attributes("-topmost", True)
|
|
||||||
splash.attributes("-alpha", 0.0)
|
|
||||||
except tk.TclError:
|
|
||||||
pass
|
|
||||||
splash.configure(bg="#0A0F1C")
|
|
||||||
splash.geometry(f"{disp_w}x{total_h}+{x}+{y}")
|
|
||||||
|
|
||||||
vid_label = tk.Label(splash, bg="#0A0F1C", borderwidth=0, highlightthickness=0)
|
|
||||||
vid_label.place(x=0, y=0, width=disp_w, height=disp_h)
|
|
||||||
|
|
||||||
tag_label = tk.Label(
|
|
||||||
splash, text=tagline,
|
|
||||||
bg="#0A0F1C", fg="#E67E22",
|
|
||||||
font=("Segoe UI", 10, "italic"),
|
|
||||||
borderwidth=0, highlightthickness=0,
|
|
||||||
)
|
|
||||||
tag_label.place(x=0, y=disp_h, width=disp_w, height=TAG_H)
|
|
||||||
|
|
||||||
state = {"closed": False, "t_start": time.time()}
|
|
||||||
|
|
||||||
def _safe_alpha(a):
|
|
||||||
with contextlib.suppress(tk.TclError):
|
|
||||||
splash.attributes("-alpha", max(0.0, min(1.0, a)))
|
|
||||||
|
|
||||||
def _fade_to(target, duration_ms, done_cb=None):
|
|
||||||
steps = max(int(duration_ms / 16), 1)
|
|
||||||
start_alpha = float(splash.attributes("-alpha") or 0.0)
|
|
||||||
|
|
||||||
def _step(i):
|
|
||||||
if state["closed"]:
|
|
||||||
return
|
|
||||||
ratio = i / steps
|
|
||||||
_safe_alpha(start_alpha + (target - start_alpha) * ratio)
|
|
||||||
if i < steps:
|
|
||||||
splash.after(16, lambda: _step(i + 1))
|
|
||||||
elif done_cb:
|
|
||||||
done_cb()
|
|
||||||
|
|
||||||
_step(0)
|
|
||||||
|
|
||||||
def _close():
|
|
||||||
if state["closed"]:
|
|
||||||
return
|
|
||||||
state["closed"] = True
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
cap.release()
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
splash.destroy()
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
root.quit()
|
|
||||||
|
|
||||||
def _play_next_frame():
|
|
||||||
if state["closed"]:
|
|
||||||
return
|
|
||||||
if (time.time() - state["t_start"]) > max_duration_s:
|
|
||||||
_fade_to(0.0, fade_ms, done_cb=_close)
|
|
||||||
return
|
|
||||||
ret, frame = cap.read()
|
|
||||||
if not ret:
|
|
||||||
# 비디오 끝 — 페이드아웃 후 종료
|
|
||||||
_fade_to(0.0, fade_ms, done_cb=_close)
|
|
||||||
return
|
|
||||||
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
||||||
if (vw, vh) != (disp_w, disp_h):
|
|
||||||
rgb = cv2.resize(rgb, (disp_w, disp_h), interpolation=cv2.INTER_AREA)
|
|
||||||
img = Image.fromarray(rgb)
|
|
||||||
photo = ImageTk.PhotoImage(img)
|
|
||||||
vid_label.configure(image=photo)
|
|
||||||
vid_label.image = photo # GC 방지
|
|
||||||
splash.after(frame_ms, _play_next_frame)
|
|
||||||
|
|
||||||
# 페이드인 → 첫 프레임 재생 시작
|
|
||||||
_fade_to(1.0, fade_ms, done_cb=_play_next_frame)
|
|
||||||
|
|
||||||
try:
|
|
||||||
root.mainloop()
|
|
||||||
finally:
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
root.destroy()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# 단독 테스트
|
|
||||||
show_intro_splash(Path(__file__).resolve().parent / "Design" / "logo_intro.mp4")
|
|
||||||
Reference in New Issue
Block a user