"""'공통 문제 상세(이미지)' 시트에 넣을 '수정 후' 이미지를 만든다 (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)