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>
This commit is contained in:
ahnyongjooandClaude Sonnet 5.5 committed 2026-10-28 11:20:18 +09:00
commit 964a841de1
3978 files changed
+58013356

No files matched your search

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