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>
This commit is contained in:
415
apply_blender_patch.py
Normal file
415
apply_blender_patch.py
Normal file
@@ -0,0 +1,415 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user