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:
2026-05-08 10:29:08 +09:00
parent 53d8b53c2f
commit b9342f6726
92 changed files with 3413501 additions and 0 deletions

415
apply_blender_patch.py Normal file
View 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)