Files
s-canvas/apply_blender_patch.py
HYUNJUNGLEE b9342f6726 Import S-CANVAS source + iter=1~7 lint cleanup
S-CANVAS (Saman Corp.) — DXF + DEM + AI 기반 3D 조감도 생성 엔진.
~24k LOC Python (scanvas_maker.py 7072 LOC GUI + 구조물 파서/빌더 다수).

이 커밋은 7-iter cleanup이 적용된 상태로 import:
- F821 8 + B023 6: 비동기 lambda + except/loop 변수 캡처 NameError
  (Py3.13에서 reproduce 확인된 진짜 버그)
- RUF012 4 + RUF013 1: ClassVar / implicit Optional 명시화
- F811/B905/B904/F401/F841/W293/F541/UP/SIM/RUF/PLR 700+ cleanup/modernization

신규 파일:
- ruff.toml: target=py313, Korean unicode/저자 스타일/도메인 복잡도 무력화
- requirements-py313.txt: pyproj>=3.7, scipy>=1.14, numpy>=2.0.2 (Py3.13 wheel)
- .gitignore: gcp-key.json, 캐시, 백업, 생성 이미지 제외

검증: ruff 0 errors, py_compile 0 errors, import 33/33 OK on Py3.13.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:29:08 +09:00

416 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""apply_blender_patch.py — scanvas_maker.py 에 Blender 렌더 통합 패치 적용.
패치 내용 (3곳):
P1) `_open_structure_template_dialog` 안에 `_do_blender_render` 콜백 추가
(기존 `_do_vlm_feedback` 함수 정의 바로 앞에 삽입)
P2) "🤖 AI 검증" 버튼 다음 줄에 "🎨 Blender 렌더" 버튼 추가
P3) 클래스 메서드로 `_show_structure_render(self, image_path)` 추가
(`_show_rendered_result` 메서드 바로 앞에 삽입)
특징:
- Idempotent: 이미 적용된 상태에서 다시 실행해도 변경 없음 (안전)
- Anchor 기반: 원본의 정확한 텍스트를 찾아 그 위치에만 삽입
anchor 못 찾으면 즉시 중단 (파일 망가뜨리지 않음)
- 백업: 실행 전 scanvas_maker.py.bak_blender 자동 생성
- 줄바꿈: 입력 파일이 CRLF면 출력도 CRLF, LF면 LF (보존)
- AST parse 검증으로 결과 syntax 체크
사용법:
cd D:\\2026\\PROGRAM\\1_S-CANVAS
python apply_blender_patch.py
옵션:
--dry-run 실제 쓰지 않고 어떤 변경이 일어날지 출력만
--no-backup 백업 파일 생성 생략
--check 이미 적용됐는지만 확인 (변경 없음)
"""
from __future__ import annotations
import argparse
import ast
import shutil
import sys
from pathlib import Path
TARGET = Path("scanvas_maker.py")
BACKUP = Path("scanvas_maker.py.bak_blender")
# ===========================================================================
# 패치 정의 (anchor + insert)
# ===========================================================================
P1_GUARD = "def _do_blender_render():"
P1_ANCHOR = " def _do_vlm_feedback():\n"
P1_INSERT = ''' def _do_blender_render():
"""Blender Cycles로 구조물 단독 고품질 렌더 (별도 트랙).
AI 워크플로(Step 4)와 별개로 실행. 결과는 'structure_render.png'.
transparent_bg=True 로 RGBA 출력 → 추후 지형 합성 입력으로도 사용 가능.
"""
try:
from blender_renderer import run_blender_render
from gate_3d_builder_bpy import dump_params_to_json as _dump_gate
except ImportError as e:
messagebox.showerror(
"모듈 없음",
f"Blender 렌더 모듈을 찾을 수 없습니다:\\n{e}\\n\\n"
"blender_renderer.py / gate_3d_builder_bpy.py 가 "
"S-CANVAS 폴더에 있는지 확인하세요.",
parent=win,
)
return
if state["params"] is None:
messagebox.showwarning(
"안내", "파라미터가 비어있습니다. '미리보기 (빌드)' 먼저 실행하세요.",
parent=win,
)
return
# 현재는 수문(spillway_gate)만 지원.
if tid != "spillway_gate":
messagebox.showinfo(
"지원 예정",
f"Blender 렌더는 현재 '여수로 수문(spillway_gate)' 템플릿만 "
f"지원합니다.\\n현재 템플릿: {tid}",
parent=win,
)
return
# 옵션 다이얼로그 — 시간대 + 투명배경 + 샘플
opt_win = ctk.CTkToplevel(win)
opt_win.title("Blender Cycles 렌더 옵션")
opt_win.geometry("420x340")
opt_win.transient(win); opt_win.grab_set()
ctk.CTkLabel(
opt_win, text="Blender Cycles 렌더 옵션",
font=ctk.CTkFont(size=14, weight="bold"),
).pack(pady=(15, 10))
ctk.CTkLabel(opt_win, text="조명 / 시간대").pack(anchor="w", padx=20)
time_var = ctk.StringVar(value="daytime")
tf = ctk.CTkFrame(opt_win, fg_color="transparent")
tf.pack(fill="x", padx=20, pady=(0, 10))
for v, lbl in [("daytime", "주간"), ("sunset", "노을"), ("overcast", "흐림")]:
ctk.CTkRadioButton(tf, text=lbl, variable=time_var, value=v).pack(side="left", padx=8)
transparent_var = ctk.BooleanVar(value=False)
ctk.CTkCheckBox(
opt_win,
text="투명 배경 (RGBA) — 지형 합성용",
variable=transparent_var,
).pack(anchor="w", padx=20, pady=(0, 8))
ctk.CTkLabel(opt_win, text="Cycles 샘플 (높을수록 깨끗·느림)").pack(anchor="w", padx=20)
samples_var = ctk.StringVar(value="128")
sf = ctk.CTkFrame(opt_win, fg_color="transparent")
sf.pack(fill="x", padx=20, pady=(0, 8))
for s in ("32", "64", "128", "256"):
ctk.CTkRadioButton(sf, text=s, variable=samples_var, value=s).pack(side="left", padx=6)
save_blend_var = ctk.BooleanVar(value=False)
save_glb_var = ctk.BooleanVar(value=False)
ctk.CTkCheckBox(opt_win, text=".blend 저장",
variable=save_blend_var).pack(anchor="w", padx=20)
ctk.CTkCheckBox(opt_win, text=".glb 저장 (외부 뷰어/VR)",
variable=save_glb_var).pack(anchor="w", padx=20, pady=(0, 6))
def _start():
try:
samples = int(samples_var.get())
except ValueError:
samples = 128
t_preset = time_var.get()
trans = bool(transparent_var.get())
save_b = bool(save_blend_var.get())
save_g = bool(save_glb_var.get())
opt_win.destroy()
try:
json_path = "gate_params.json"
_dump_gate(state["params"], json_path)
self.log(f" [{name}] GateParams -> {json_path}")
except Exception as e:
messagebox.showerror("JSON 저장 실패",
f"GateParams JSON 직렬화 실패:\\n{e}", parent=win)
return
self.log(f" [{name}] Blender 렌더 시작 "
f"(time={t_preset}, samples={samples}, "
f"bg={'투명' if trans else 'sky'})")
threading.Thread(
target=run_blender_render,
args=(self, None, json_path),
kwargs=dict(
time_preset=t_preset,
engine="CYCLES",
samples=samples,
output_path="structure_render.png",
transparent_bg=trans,
save_blend=save_b,
save_glb=save_g,
structure_kind="gate",
),
daemon=True,
).start()
bf = ctk.CTkFrame(opt_win, fg_color="transparent")
bf.pack(fill="x", pady=15, padx=20)
ctk.CTkButton(bf, text="취소", width=80,
fg_color="transparent", border_width=1,
command=opt_win.destroy).pack(side="left")
ctk.CTkButton(bf, text="🎨 렌더 시작", width=140,
fg_color="#16A085", hover_color="#117A65",
text_color="white",
command=_start).pack(side="right")
''' + P1_ANCHOR
P2_GUARD = '🎨 Blender 렌더'
P2_ANCHOR = (
' ctk.CTkButton(bottom, text="🤖 AI 검증", width=110,\n'
' fg_color="#D35400", hover_color="#A04000",\n'
' text_color="white",\n'
' font=ctk.CTkFont(size=11, weight="bold"),\n'
' command=_do_vlm_feedback).pack(side="right", padx=3)\n'
)
P2_INSERT = P2_ANCHOR + (
' ctk.CTkButton(bottom, text="🎨 Blender 렌더", width=140,\n'
' fg_color="#16A085", hover_color="#117A65",\n'
' text_color="white",\n'
' font=ctk.CTkFont(size=11, weight="bold"),\n'
' command=_do_blender_render).pack(side="right", padx=3)\n'
)
P3_GUARD = "def _show_structure_render(self,"
P3_ANCHOR = " def _show_rendered_result(self, image_path):\n"
P3_INSERT = ''' def _show_structure_render(self, image_path):
"""Blender 구조물 렌더 결과를 별도 창에 표시 (AI 결과와 분리).
blender_renderer.py 가 호출. 투명 PNG도 정상 표시(체커보드 배경).
"""
try:
from PIL import ImageTk, Image as _PILImage
try:
pil_img = _PILImage.open(image_path)
except Exception as e:
messagebox.showerror("이미지 열기 실패",
f"렌더 결과 PNG를 열 수 없습니다:\\n{image_path}\\n\\n{e}")
return
# 투명 PNG는 체커보드 배경 위에 합성 표시
display_img = pil_img
if pil_img.mode == "RGBA":
from PIL import ImageDraw
tile = _PILImage.new("RGB", (16, 16), (200, 200, 200))
d = ImageDraw.Draw(tile)
d.rectangle((0, 0, 7, 7), fill=(170, 170, 170))
d.rectangle((8, 8, 15, 15), fill=(170, 170, 170))
bg = _PILImage.new("RGB", pil_img.size, (200, 200, 200))
for y in range(0, pil_img.size[1], 16):
for x in range(0, pil_img.size[0], 16):
bg.paste(tile, (x, y))
display_img = _PILImage.alpha_composite(
bg.convert("RGBA"), pil_img
).convert("RGB")
win = ctk.CTkToplevel(self)
win.title(f"🎨 Blender 렌더 결과 - {Path(image_path).name}")
sw = self.winfo_screenwidth(); sh = self.winfo_screenheight()
max_w, max_h = int(sw * 0.7), int(sh * 0.75)
iw, ih = display_img.size
scale = min(max_w / iw, max_h / ih, 1.0)
disp_w, disp_h = int(iw * scale), int(ih * scale)
disp = display_img.resize((disp_w, disp_h), _PILImage.LANCZOS)
tk_img = ImageTk.PhotoImage(disp)
lbl = ctk.CTkLabel(win, text="", image=tk_img)
lbl.image = tk_img
lbl.pack(padx=10, pady=10)
info = ctk.CTkFrame(win, fg_color="transparent")
info.pack(fill="x", padx=10, pady=(0, 5))
mode_str = "투명 배경 (RGBA, 합성용)" if pil_img.mode == "RGBA" else "Sky 배경 (RGB)"
ctk.CTkLabel(
info,
text=f"파일: {image_path} · 원본 {iw}×{ih} · {mode_str}",
font=ctk.CTkFont(size=11),
).pack(side="left", padx=5)
btnf = ctk.CTkFrame(win, fg_color="transparent")
btnf.pack(fill="x", padx=10, pady=(0, 10))
def _open_external():
try:
if sys.platform == "win32":
os.startfile(image_path)
elif sys.platform == "darwin":
import subprocess
subprocess.Popen(["open", image_path])
else:
import subprocess
subprocess.Popen(["xdg-open", image_path])
except Exception as e:
messagebox.showerror("열기 실패",
f"기본 뷰어 실행 실패:\\n{e}", parent=win)
def _open_folder():
try:
folder = str(Path(image_path).resolve().parent)
if sys.platform == "win32":
os.startfile(folder)
elif sys.platform == "darwin":
import subprocess
subprocess.Popen(["open", folder])
else:
import subprocess
subprocess.Popen(["xdg-open", folder])
except Exception as e:
messagebox.showerror("폴더 열기 실패", f"{e}", parent=win)
ctk.CTkButton(btnf, text="📂 폴더 열기", width=110,
command=_open_folder).pack(side="right", padx=3)
ctk.CTkButton(btnf, text="🖼 외부 뷰어로 열기", width=160,
command=_open_external).pack(side="right", padx=3)
ctk.CTkButton(btnf, text="닫기", width=80,
fg_color="transparent", border_width=1,
command=win.destroy).pack(side="left", padx=3)
except Exception as e:
import traceback
traceback.print_exc()
messagebox.showerror("결과 창 오류", f"렌더 결과 창 생성 실패:\\n{e}")
''' + P3_ANCHOR
# ===========================================================================
# 메인
# ===========================================================================
def detect_eol(raw_bytes: bytes) -> str:
"""원본 파일의 줄바꿈 형식 감지. 패치 후 보존."""
crlf = raw_bytes.count(b"\r\n")
lf = raw_bytes.count(b"\n") - crlf
if crlf > lf:
return "\r\n"
return "\n"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--dry-run", action="store_true",
help="실제 쓰지 않고 변경 사항만 출력")
ap.add_argument("--no-backup", action="store_true",
help="백업 파일 생성 생략")
ap.add_argument("--check", action="store_true",
help="패치 적용 상태만 확인 (변경 없음)")
args = ap.parse_args()
if not TARGET.is_file():
sys.exit(f"[ERR] {TARGET} 가 현재 폴더에 없습니다. "
f"D:\\2026\\PROGRAM\\1_S-CANVAS 에서 실행하세요.")
raw = TARGET.read_bytes()
eol = detect_eol(raw)
print(f"파일: {TARGET} ({len(raw):,} bytes, EOL={'CRLF' if eol == chr(13) + chr(10) else 'LF'})")
# 항상 LF로 정규화해서 작업 (anchor 매칭 일관성)
src = raw.decode("utf-8").replace("\r\n", "\n")
original_src = src
# 적용 상태 점검
states = {
"P1 (callback _do_blender_render)": P1_GUARD in src,
"P2 (button '🎨 Blender 렌더')": P2_GUARD in src,
"P3 (method _show_structure_render)": P3_GUARD in src,
}
all_applied = all(states.values())
none_applied = not any(states.values())
print("\n현재 상태:")
for name, applied in states.items():
mark = "✓ 적용됨" if applied else " 미적용"
print(f" {mark} {name}")
if args.check:
if all_applied:
print("\n[CHECK] 모든 패치 적용됨")
sys.exit(0)
elif none_applied:
print("\n[CHECK] 패치 미적용")
sys.exit(1)
else:
print("\n[CHECK] 부분 적용 — 권장: --dry-run 으로 확인 후 정상 실행")
sys.exit(2)
if all_applied:
print("\n→ 모든 패치 이미 적용됨. 추가 작업 없음.")
return 0
# 적용
changes = []
if P1_GUARD not in src:
if P1_ANCHOR not in src:
sys.exit("[ERR] P1 anchor 'def _do_vlm_feedback():' 못 찾음. "
"scanvas_maker.py 구조가 변경되었을 수 있습니다.")
src = src.replace(P1_ANCHOR, P1_INSERT, 1)
changes.append("P1: callback _do_blender_render 추가")
if P2_GUARD not in src:
if P2_ANCHOR not in src:
sys.exit("[ERR] P2 anchor (🤖 AI 검증 버튼 블록) 못 찾음.")
src = src.replace(P2_ANCHOR, P2_INSERT, 1)
changes.append("P2: 🎨 Blender 렌더 버튼 추가")
if P3_GUARD not in src:
if P3_ANCHOR not in src:
sys.exit("[ERR] P3 anchor 'def _show_rendered_result' 못 찾음.")
src = src.replace(P3_ANCHOR, P3_INSERT, 1)
changes.append("P3: method _show_structure_render 추가")
print("\n적용할 변경:")
for c in changes:
print(f" + {c}")
print(f"\n 파일 크기: {len(original_src):,}{len(src):,} chars "
f"({len(src) - len(original_src):+,})")
# AST parse 검증
try:
ast.parse(src)
print(f" AST parse: OK ({len(src.splitlines()):,} lines)")
except SyntaxError as e:
sys.exit(f"\n[ERR] 패치 후 syntax error 발생: {e}\n"
f"파일 변경 안 함. 원본 유지.")
if args.dry_run:
print("\n[DRY RUN] 실제 파일은 변경하지 않았습니다. 적용하려면 --dry-run 빼고 재실행.")
return 0
# 백업
if not args.no_backup:
shutil.copy2(TARGET, BACKUP)
print(f"\n 백업: {BACKUP}")
# 원본 EOL로 복원해서 쓰기
final_bytes = src.encode("utf-8")
if eol == "\r\n":
final_bytes = final_bytes.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")
TARGET.write_bytes(final_bytes)
print(f"\n✓ 패치 적용 완료: {TARGET} ({len(final_bytes):,} bytes)")
print("\n다음 단계:")
print(" 1) S-CANVAS 실행: python scanvas_maker.py")
print(" 2) Step 2에서 도면 로드 → 구조물 식별")
print(" 3) 사이드바 '구조물 상세 빌드' 다이얼로그 → 수문 선택")
print(" 4) 파라미터 조정 후 '🗔 미리보기' 또는 '🎨 Blender 렌더'")
if __name__ == "__main__":
sys.exit(main() or 0)