- 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>
135 lines
4.8 KiB
Python
135 lines
4.8 KiB
Python
"""도면↔모델 불일치 시각자료 합성기 (운곡교 M-5 이미지 형식: 헤더 / 도면 패널(파랑) / 모델 패널(주황·노랑·회색) / 차이·도면 쪽·조치).
|
|
사용: from compose_new import make; make(cfg)
|
|
cfg = {code, title, subtitle, color:(r,g,b), out,
|
|
panels:[{label, color, images:[{path, crop(optional), cap(optional)}], maxh}], rows:[(라벨, 본문)]}
|
|
"""
|
|
import os, sys
|
|
sys.path.insert(0, r"C:\Users\User\AppData\Local\Temp\claude\D--Rhino-MCP-rhino-mcp-main\f46b47bc-9105-4079-9b62-fcbee31b26ae\scratchpad\pylib")
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
FN = r"C:\Windows\Fonts\malgun.ttf"
|
|
FB = r"C:\Windows\Fonts\malgunbd.ttf"
|
|
W = 2400
|
|
PAD = 40
|
|
BLUE = (60, 120, 200)
|
|
ORANGE = (230, 110, 20)
|
|
DYEL = (190, 150, 0)
|
|
GREY = (120, 120, 120)
|
|
|
|
|
|
def font(sz, bold=False):
|
|
return ImageFont.truetype(FB if bold else FN, sz)
|
|
|
|
|
|
def fit(img, w, h):
|
|
r = min(w / img.width, h / img.height)
|
|
return img.resize((max(1, int(img.width * r)), max(1, int(img.height * r))), Image.LANCZOS)
|
|
|
|
|
|
def wrap(draw, text, fnt, maxw):
|
|
lines = []
|
|
for para in text.split("\n"):
|
|
cur = ""
|
|
for ch in para:
|
|
if draw.textlength(cur + ch, font=fnt) > maxw:
|
|
lines.append(cur)
|
|
cur = ch
|
|
else:
|
|
cur += ch
|
|
lines.append(cur)
|
|
return lines
|
|
|
|
|
|
def autocrop(im, pad=30, thr=14):
|
|
bg = im.getpixel((2, 2))
|
|
px = im.load()
|
|
w, h = im.size
|
|
x0, y0, x1, y1 = w, h, 0, 0
|
|
for y in range(0, h, 2):
|
|
for x in range(0, w, 2):
|
|
p = px[x, y]
|
|
if abs(p[0] - bg[0]) + abs(p[1] - bg[1]) + abs(p[2] - bg[2]) > thr:
|
|
if x < x0: x0 = x
|
|
if x > x1: x1 = x
|
|
if y < y0: y0 = y
|
|
if y > y1: y1 = y
|
|
if x1 <= x0:
|
|
return im
|
|
return im.crop((max(0, x0 - pad), max(0, y0 - pad), min(w, x1 + pad), min(h, y1 + pad)))
|
|
|
|
|
|
def load(spec):
|
|
im = Image.open(spec["path"]).convert("RGB")
|
|
if spec.get("auto", spec["path"].find("shots") >= 0):
|
|
im = autocrop(im)
|
|
if spec.get("crop"):
|
|
im = im.crop(spec["crop"])
|
|
if spec.get("draw"):
|
|
spec["draw"](im)
|
|
return im
|
|
|
|
|
|
def make(cfg):
|
|
PW = W - 2 * PAD
|
|
# 패널 이미지 준비
|
|
prepared = []
|
|
for p in cfg["panels"]:
|
|
ims = [load(s) for s in p["images"]]
|
|
n = len(ims)
|
|
gap = 20
|
|
cw = (PW - 8 - gap * (n - 1)) // n
|
|
maxh = p.get("maxh", 560)
|
|
fitted = [fit(im, cw, maxh) for im in ims]
|
|
ph = max(f.height for f in fitted) + 8
|
|
capsp = 0
|
|
if any(s.get("cap") for s in p["images"]):
|
|
capsp = 52
|
|
prepared.append((p, fitted, ph, capsp, cw, gap))
|
|
# 본문 줄 계산
|
|
dtmp = ImageDraw.Draw(Image.new("RGB", (10, 10)))
|
|
fr = font(32)
|
|
rows = []
|
|
for lab, text in cfg["rows"]:
|
|
lines = wrap(dtmp, text, fr, PW - 230)
|
|
rows.append((lab, lines))
|
|
H = 120 + 14
|
|
for p, fitted, ph, capsp, cw, gap in prepared:
|
|
H += 50 + 6 + ph + capsp + 14
|
|
H += 12
|
|
for lab, lines in rows:
|
|
H += max(46, 42 * len(lines)) + 12
|
|
H += 24
|
|
img = Image.new("RGB", (W, H), (255, 255, 255))
|
|
d = ImageDraw.Draw(img)
|
|
# 헤더
|
|
d.rectangle([0, 0, W, 120], fill=(38, 44, 56))
|
|
d.rounded_rectangle([PAD - 6, 24, PAD + 130, 96], radius=10, fill=cfg.get("color", ORANGE))
|
|
d.text((PAD + 62, 60), cfg["code"], font=font(52, True), fill=(255, 255, 255), anchor="mm")
|
|
d.text((PAD + 170, 14), cfg["title"], font=font(48, True), fill=(255, 255, 255))
|
|
d.text((PAD + 170, 74), cfg["subtitle"], font=font(30), fill=(210, 215, 225))
|
|
y = 134
|
|
for p, fitted, ph, capsp, cw, gap in prepared:
|
|
col = p.get("color", BLUE)
|
|
d.rectangle([PAD - 6, y, W - PAD + 6, y + 50], fill=col)
|
|
d.text((PAD + 10, y + 25), p["label"], font=font(32, True), fill=(255, 255, 255), anchor="lm")
|
|
y += 56
|
|
d.rectangle([PAD - 6, y, W - PAD + 6, y + ph + capsp + 4], outline=col, width=4)
|
|
x = PAD
|
|
for f, spec in zip(fitted, p["images"]):
|
|
img.paste(f, (x, y + 4))
|
|
if spec.get("cap"):
|
|
d.text((x + 6, y + 4 + f.height + 4), spec["cap"], font=font(30, True), fill=(30, 30, 30))
|
|
x += cw + gap
|
|
y += ph + capsp + 18
|
|
y += 6
|
|
for lab, lines in rows:
|
|
h = max(46, 42 * len(lines))
|
|
d.rounded_rectangle([PAD - 6, y, PAD + 180, y + 44], radius=8, fill=(232, 236, 244))
|
|
d.text((PAD + 87, y + 22), lab, font=font(30, True), fill=(30, 30, 40), anchor="mm")
|
|
for i, ln in enumerate(lines):
|
|
d.text((PAD + 210, y + 4 + 42 * i), ln, font=fr, fill=(25, 25, 25))
|
|
y += h + 12
|
|
os.makedirs(os.path.dirname(cfg["out"]), exist_ok=True)
|
|
img.save(cfg["out"])
|
|
return img.size
|