Files
Pier_v1/Code/검토수정/스크립트/make_after_images.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

213 lines
13 KiB
Python
Raw 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.
"""'공통 문제 상세(이미지)' 시트에 넣을 '수정 후' 이미지를 만든다 (2026-10-26).
C-7 (기초 철근 높이): 수정된 모델의 Rhino Front / Right 뷰 캡처(Code/검토수정/결과/캡처/*.png, c1_capture.py 로 생성)에
높이 기준선과 라벨, 제목 배너, 하단 설명을 얹는다.
C-1 (굽힘 반지름): 수정된 모델에서 확인한 호 반지름(KDS 확인 결과 JSON)으로 기존 이미지와 같은 형식(굽힘 평면도 + 확대)을 그린다.
출력: 검토파일/수정후_캡처/*.png
"""
import json
import math
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib import font_manager
from matplotlib.patches import Arc, Circle
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(r"D:\★Pier 자료\프로그램\Pier_T")
CAP = ROOT / "Code" / "검토수정" / "결과" / "캡처"
OUT = ROOT / "검토파일" / "수정후_캡처"
OUT.mkdir(parents=True, exist_ok=True)
FONT = r"C:\Windows\Fonts\malgun.ttf"
FONTB = r"C:\Windows\Fonts\malgunbd.ttf"
font_manager.fontManager.addfont(FONT)
font_manager.fontManager.addfont(FONTB)
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
def F(size, bold=False):
return ImageFont.truetype(FONTB if bold else FONT, size)
# ------------------------------------------------------------------ C-7 images
C7 = {
"ungok": {"name": "운곡교", "H": 2200, "lines": [("B5 (H29 상면 바깥)", 2100, "top", "m"), ("B1 (상면 안쪽)", 2072.5, "top", "g"),
("B2·B4 (하면 안쪽)", 177, "bot", "g"), ("B6·B8 (H29 하면 바깥)", 150, "bot", "m")],
"note": "바깥층 B5(상면 100)·B6(하면 150), 나머지는 안쪽에 접촉 배치"},
"sanoe": {"name": "산외교", "H": 2500, "lines": [("B1 (H16 상면 바깥)", 2400, "top", "m"), ("B4 (상면 안쪽)", 2384, "top", "g"),
("B5·B6 (H22 하면 안쪽)", 172, "bot", "g"), ("B2·B3 (H22 하면 바깥)", 150, "bot", "m")],
"note": "바깥층 상면 100·하면 150, 나머지는 안쪽에 접촉 배치 (직경 동일)"},
"okseong": {"name": "옥성교", "H": 2200, "lines": [("B1·B3 (H29 상면 바깥)", 2100, "top", "m"), ("B5·B7 (상면 안쪽)", 2071, "top", "g"),
("B6·B8 (하면 안쪽)", 179, "bot", "g"), ("B2·B4 (H29 하면 바깥)", 150, "bot", "m")],
"note": "B1~B4 는 원래 2100 / 150, 안쪽 B5~B8 을 접촉 위치로 이동 (직경 동일)"},
}
def compose_c7(key, view):
spec = C7[key]
cj = json.loads((CAP / ("%s_capture.json" % key)).read_text(encoding="utf-8"))
info = cj["views"][view]
img = Image.open(CAP / ("%s_%s.png" % (key, view))).convert("RGB")
lines = info["lines"]
ys = [p[1] for v in lines.values() for p in v]
xs = [p[0] for v in lines.values() for p in v]
# footing frame in pixels: from the extreme lines, extended to the footing faces (z 0 and z H) by linear scaling
keys = list(lines.keys())
z = {k: spec["lines"][i][1] for i, k in enumerate(keys)}
k1, k2 = keys[0], keys[-1]
y1, y2 = lines[k1][0][1], lines[k2][0][1]
pxmm = (y2 - y1) / (z[k2] - z[k1]) # pixels per mm (negative: z up = y down)
ytop = y1 + (spec["H"] - z[k1]) * pxmm
ybot = y1 + (0 - z[k1]) * pxmm
xl, xr = min(xs), max(xs)
pad_l, pad_r, pad_t, pad_b = 430, 40, 90, 90
box = (max(0, int(xl - pad_l)), max(0, int(min(ytop, ybot) - pad_t)), min(img.width, int(xr + pad_r)), min(img.height, int(max(ytop, ybot) + pad_b)))
crop = img.crop(box)
d = ImageDraw.Draw(crop)
ox, oy = box[0], box[1]
for fy in (ytop, ybot): # footing top / bottom face
d.line([(xl - ox, fy - oy), (xr - ox, fy - oy)], fill=(40, 40, 160), width=2)
d.text((xr - ox - 130, ytop - oy - 34), "기초 상면", font=F(22, True), fill=(40, 40, 160))
d.text((xr - ox - 130, ybot - oy + 8), "기초 하면", font=F(22, True), fill=(40, 40, 160))
entries = []
for label, zrel, side, col in spec["lines"]:
yy = y1 + (zrel - z[k1]) * pxmm - oy
color = (200, 0, 200) if col == "m" else (0, 130, 0)
x = xl - ox
while x < xr - ox: # dashed reference line
d.line([(x, yy), (min(x + 18, xr - ox), yy)], fill=color, width=2)
x += 30
cover = (spec["H"] - zrel) if side == "top" else zrel
txt = "%s z %g (%s에서 %g)" % (label, zrel, "상면" if side == "top" else "하면", cover)
entries.append({"y": yy, "ly": yy, "txt": txt, "color": color})
# spread the labels so they do not overlap (min 34 px apart), keep order, groups stay near their lines
entries.sort(key=lambda e: e["y"])
for i in range(1, len(entries)):
if entries[i]["ly"] - entries[i - 1]["ly"] < 34:
entries[i]["ly"] = entries[i - 1]["ly"] + 34
mid_shift = (sum(e["y"] for e in entries) - sum(e["ly"] for e in entries)) / len(entries)
for e in entries:
e["ly"] += mid_shift * 0.0
for i in range(len(entries) - 1, 0, -1): # if pushed below the footing bottom area, pull back up
if entries[i]["ly"] > crop.height - 40:
entries[i]["ly"] = crop.height - 40
if entries[i]["ly"] - entries[i - 1]["ly"] < 34:
entries[i - 1]["ly"] = entries[i]["ly"] - 34
for e in entries:
w = d.textlength(e["txt"], font=F(21, True))
tx = max(6, xl - ox - w - 40)
d.line([(tx + w + 6, e["ly"]), (xl - ox - 4, e["y"])], fill=e["color"], width=2) # leader to the line
d.rectangle([tx - 4, e["ly"] - 15, tx + w + 4, e["ly"] + 15], fill=(255, 255, 255), outline=e["color"])
d.text((tx, e["ly"] - 13), e["txt"], font=F(21, True), fill=e["color"])
title = "%s · 수정 후 모델 %s (라이노 %s 뷰)" % (spec["name"], "정면도" if view == "Front" else "단면도", view)
sub = "기초 철근 중심 높이 — 기초 하면 기준 z (mm), 괄호 = 해당 면에서 철근 중심까지 거리"
W = crop.width
banner = Image.new("RGB", (W, 112), (30, 120, 60))
bd = ImageDraw.Draw(banner)
bd.text((14, 8), title, font=F(40, True), fill=(255, 255, 255))
bd.text((14, 64), sub, font=F(26), fill=(255, 255, 255))
foot = Image.new("RGB", (W, 74), (246, 246, 246))
fd = ImageDraw.Draw(foot)
fd.rectangle([14, 12, 40, 38], fill=(200, 0, 200))
fd.text((50, 8), "자홍 = 바깥층(피복 기준, 도면 철근선)", font=F(24), fill=(0, 0, 0))
fd.rectangle([W // 2 + 10, 12, W // 2 + 36, 38], fill=(0, 130, 0))
fd.text((W // 2 + 46, 8), "녹색 = 안쪽층(바깥층에 접촉)", font=F(24), fill=(0, 0, 0))
fd.text((14, 42), spec["note"] + " ※ 기초 콘크리트는 숨기고 철근만 표시", font=F(22), fill=(60, 60, 60))
out = Image.new("RGB", (W, banner.height + crop.height + foot.height), (255, 255, 255))
out.paste(banner, (0, 0))
out.paste(crop, (0, banner.height))
out.paste(foot, (0, banner.height + crop.height))
p = OUT / ("C-7_%s_수정후_%s.png" % ("정면도" if view == "Front" else "단면도", spec["name"]))
out.save(p)
return p
# ------------------------------------------------------------------ C-1 images
NOM = {13: 12.7, 16: 15.9, 19: 19.1, 22: 22.2, 25: 25.4, 29: 28.6, 32: 31.8}
def compose_c1(bridge, dia, R_old, R_new, row_title, changed=True, hook=False):
d = int(dia[1:])
nom = NOM[d]
k = 3.5 if d >= 19 else 2.0
inner_actual = R_new - d / 2.0
need_inner_d = k * nom
need_inner_R = need_inner_d / 2.0
diff_d = 2 * inner_actual - need_inner_d
fig = plt.figure(figsize=(10, 6.05), dpi=100)
fig.patch.set_facecolor("white")
head = fig.add_axes([0, 0.66, 1, 0.34])
head.set_facecolor((0.09, 0.45, 0.22))
head.set_xticks([]); head.set_yticks([])
for s in head.spines.values():
s.set_visible(False)
head.set_xlim(0, 1); head.set_ylim(0, 1)
head.text(0.012, 0.80, "%s · %s %s — 수정 후" % (bridge, dia, row_title), color="white", fontsize=19, fontweight="bold", va="center")
if changed:
head.text(0.012, 0.58, "중심선 R %.2f (수정 전 %.2f) 파이프 지름 %d 바깥면 R %.2f" % (R_new, R_old, d, R_new + d / 2.0), color="white", fontsize=11.5, va="center")
else:
head.text(0.012, 0.58, "중심선 R %.2f (수정 없음, 이미 기준 충족) 파이프 지름 %d 바깥면 R %.2f" % (R_new, d, R_new + d / 2.0), color="white", fontsize=11.5, va="center")
head.text(0.012, 0.40, "내면 실측: 중심선에서 %.1f(직경/2) 안쪽 → R %.2f, 내면지름 %.2f" % (d / 2.0, inner_actual, 2 * inner_actual), color="white", fontsize=11.5, va="center")
head.text(0.012, 0.22, "KDS 필요: %.1f × 공칭 %.1f = 내면지름 %.2f (필요 R %.3f)" % (k, nom, need_inner_d, need_inner_R), color="white", fontsize=11.5, va="center")
ok = diff_d >= -0.005
head.text(0.012, 0.06, "차이(실측-필요): 내면지름 %+.2f mm → %s" % (diff_d, "충족" if ok else "미달"), color=(1, 1, 0.3), fontsize=14, fontweight="bold", va="center")
ax1 = fig.add_axes([0.01, 0.06, 0.48, 0.58])
ax2 = fig.add_axes([0.51, 0.06, 0.48, 0.58])
for ax in (ax1, ax2):
ax.set_facecolor((0.62, 0.65, 0.68))
ax.set_aspect("equal")
ax.set_xticks([]); ax.set_yticks([])
if hook: # 180 deg hook elevation: vertical leg, semicircle, tail
R = R_new
ax1.add_patch(Arc((R, 0), 2 * R, 2 * R, theta1=0, theta2=180, color="magenta", lw=2))
ax1.plot([0, 0], [-150, 0], color="magenta", lw=2)
ax1.plot([2 * R, 2 * R], [0, -100], color="magenta", lw=2)
ax1.add_patch(Arc((R, 0), 2 * (R + d / 2), 2 * (R + d / 2), theta1=0, theta2=180, color="black", lw=1))
ax1.add_patch(Arc((R, 0), 2 * inner_actual, 2 * inner_actual, theta1=0, theta2=180, color="blue", lw=1.5))
ax1.add_patch(Arc((R, 0), 2 * need_inner_R, 2 * need_inner_R, theta1=0, theta2=180, color="red", lw=1.2))
ax1.text(R, -60, "180° 갈고리 R%.0f · 연장 100" % R, ha="center", fontsize=9)
ax1.set_xlim(-60, 2 * R + 60); ax1.set_ylim(-170, R + 40)
else:
cx, cy = 0.0, 0.0
ax1.add_patch(Arc((cx, cy), 2 * R_new, 2 * R_new, theta1=0, theta2=90, color="magenta", lw=2))
ax1.add_patch(Arc((cx, cy), 2 * (R_new + d / 2), 2 * (R_new + d / 2), theta1=0, theta2=90, color="black", lw=1))
ax1.add_patch(Arc((cx, cy), 2 * inner_actual, 2 * inner_actual, theta1=0, theta2=90, color="blue", lw=1.5))
ax1.add_patch(Arc((cx, cy), 2 * need_inner_R, 2 * need_inner_R, theta1=0, theta2=90, color="red", lw=1.2))
ax1.add_patch(Circle((R_new, 0), d / 2, fill=False, color="gray", lw=0.8))
ax1.add_patch(Circle((0, R_new), d / 2, fill=False, color="gray", lw=0.8))
ax1.set_xlim(-10, R_new + d); ax1.set_ylim(-10, R_new + d)
ax1.text(0.02, 0.02, "전체 보기 (%s)" % ("180° 갈고리" if hook else "90° 굽힘, 평면도"), transform=ax1.transAxes, fontsize=9, color="white")
# zoom: 10 mm window around the inner surface at 45 degrees (blue = actual, red = required)
ang = math.radians(45)
for r_, col, lw in ((inner_actual, "blue", 1.6), (need_inner_R, "red", 1.2)):
t = [math.radians(a) for a in range(20, 71)]
ax2.plot([r_ * math.cos(a) for a in t], [r_ * math.sin(a) for a in t], color=col, lw=lw)
mx, my = inner_actual * math.cos(ang), inner_actual * math.sin(ang)
ax2.set_xlim(mx - 5, mx + 5); ax2.set_ylim(my - 3, my + 3)
ax2.text(0.02, 0.02, "확대: 폭 10 mm 파랑(실측)·빨강(KDS 필요) 간격 = 차이", transform=ax2.transAxes, fontsize=9, color="white")
foot = fig.add_axes([0, 0, 1, 0.055]); foot.axis("off")
foot.text(0.01, 0.4, "수정 완료 (2026-10-26) — 중심선 R %.2f → 내면지름 %.2f mm ≥ KDS 필요 %.2f mm" % (R_new, 2 * inner_actual, need_inner_d), fontsize=9.5, va="center")
p = OUT / ("C-1_%s_%s_수정후.png" % (bridge, dia))
fig.savefig(p, dpi=100, facecolor="white")
plt.close(fig)
return p
if __name__ == "__main__":
made = []
for key in ("ungok", "sanoe", "okseong"):
for view in ("Front", "Right"):
made.append(compose_c7(key, view))
C1 = [("운곡교", "H29", 65.25, 65.25, "90° 굽힘", False), ("옥성교", "H29", 65.25, 65.25, "90° 굽힘", False), ("Ramp-A교", "H29", 65.25, 65.25, "90° 굽힘", False),
("운곡교", "H25", 56.25, 56.95, "90° 굽힘", True), ("옥성교", "H25", 56.25, 56.95, "90° 굽힘", True), ("Ramp-A교", "H25", 56.25, 56.95, "90° 굽힘", True),
("산외교", "H22", 49.5, 49.85, "굽힘", True), ("옥성교", "H22", 49.5, 49.85, "굽힘", True),
("운곡교", "H19", 42.75, 42.93, "90° 굽힘", True), ("산외교", "H19", 42.75, 42.93, "90° 굽힘", True), ("옥성교", "H19", 42.75, 42.93, "90° 굽힘", True), ("Ramp-A교", "H19", 42.75, 42.93, "90° 굽힘", True)]
for b, dia, ro, rn, t, ch in C1:
made.append(compose_c1(b, dia, ro, rn, t, ch))
made.append(compose_c1("운곡교", "H16", 20.37, 24.0, "180° 갈고리 (B9)", True, hook=True))
for p in made:
print(p.name)