- 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>
97 lines
4.2 KiB
Python
97 lines
4.2 KiB
Python
"""Template recommendation from a drawing set (S02, 2026-10-15): estimate the fit questions from the drawing texts and
|
|
rank the protected templates by the number of matching answers.
|
|
|
|
Only questions the texts can answer are estimated; the rest stay None ("판단 불가") and are answered by the user.
|
|
Estimates are proposals with their evidence, never confirmations. Rules were checked on 9 drawing sets only
|
|
(Ungok P1-P4, Ulsan P1-P2, Ramp-A P1, Okseong P1, Saejae-1 P1); weak ones are marked.
|
|
"""
|
|
import re
|
|
|
|
Q_PILE, Q_SOFFIT, Q_BEARING = "foundation", "soffit", "bearings"
|
|
QUESTIONS = [
|
|
(Q_PILE, "기초 형식(말뚝 유무)"),
|
|
(Q_SOFFIT, "코핑 하면 형상"),
|
|
(Q_BEARING, "받침 개수"),
|
|
]
|
|
R_RE = re.compile(r"(?:^|[\s(])R\s*=?\s*(\d[\d,]*(?:\.\d+)?)\s*$")
|
|
|
|
|
|
def profile(params):
|
|
"""Answers a template's params.json stands for."""
|
|
piles = "piles" in params or "pile" in params
|
|
cp = params.get("coping", {})
|
|
if "soffit_radius" in cp:
|
|
soffit = "곡선"
|
|
elif cp.get("form") == "y_shaft":
|
|
soffit = "나팔형"
|
|
else:
|
|
soffit = "직선 경사"
|
|
found = "말뚝만" if ("footing" not in params and piles) else ("기초판+말뚝" if piles else "기초판만")
|
|
return {Q_PILE: found, Q_SOFFIT: soffit, Q_BEARING: len(params.get("bearing_seats", {}).get("bearings", []))}
|
|
|
|
|
|
def _r_mm(text):
|
|
m = R_RE.search(text.strip())
|
|
if not m:
|
|
return None
|
|
s = m.group(1)
|
|
return round(float(s) * 1000) if re.fullmatch(r"\d+\.\d{3}", s) else round(float(s.replace(",", "")))
|
|
|
|
|
|
def _where(t):
|
|
return f"{t.get('dwg', '?')} {t.get('layer', '')} ({t['x']:.0f}, {t['y']:.0f}) '{t['text'][:40]}'"
|
|
|
|
|
|
def count_bearings(rows):
|
|
"""Largest run 1..n of integer first cells under the ELEVATION TABLE title."""
|
|
n = 0
|
|
for r in rows:
|
|
c = r["cells"][0]["text"].strip() if r.get("cells") else ""
|
|
if c.isdigit() and int(c) == n + 1:
|
|
n += 1
|
|
return n
|
|
|
|
|
|
def estimate(texts, table_rows=None):
|
|
"""{question: (answer or None, evidence str, confidence)}; table_rows = rows of the ELEVATION TABLE (reader.table_candidates)."""
|
|
out = {}
|
|
piles = [t for t in texts if "말뚝" in t["text"]]
|
|
cast = [t for t in piles if "현장타설말뚝" in t["text"]]
|
|
if cast:
|
|
out[Q_PILE] = ("말뚝만", "현장타설말뚝 문자: " + _where(cast[0]), "약함")
|
|
elif piles:
|
|
out[Q_PILE] = ("기초판+말뚝", f"'말뚝' 문자 {len(piles)}개: " + _where(piles[0]), "보통")
|
|
else:
|
|
out[Q_PILE] = ("기초판만", "'말뚝' 문자 없음", "보통")
|
|
rs = [(v, t) for t in texts if len(t["text"]) < 30 and (v := _r_mm(t["text"])) is not None]
|
|
big = [(v, t) for v, t in rs if v >= 10000]
|
|
mid = [(v, t) for v, t in rs if 1000 < v < 10000]
|
|
if big:
|
|
out[Q_SOFFIT] = ("곡선", f"R {big[0][0]} mm: " + _where(big[0][1]), "보통")
|
|
elif mid:
|
|
out[Q_SOFFIT] = ("나팔형", f"R {mid[0][0]} mm: " + _where(mid[0][1]), "약함")
|
|
else:
|
|
small = f"(R {rs[0][0]} mm는 평면 모서리 등으로 보고 제외)" if rs else ""
|
|
out[Q_SOFFIT] = ("직선 경사", "정면도 R 문자 없음 " + small, "약함")
|
|
n = count_bearings(table_rows) if table_rows else 0
|
|
out[Q_BEARING] = (n, f"ELEVATION TABLE 행 {n}개", "보통") if n else (None, "ELEVATION TABLE을 찾지 못함", "")
|
|
return out
|
|
|
|
|
|
def rank(est, profiles):
|
|
"""Templates ordered by the number of estimated answers they match: [{name, matched, asked, hits{q: bool}}]."""
|
|
asked = [q for q, _ in QUESTIONS if est.get(q, (None,))[0] is not None]
|
|
rows = []
|
|
for name, prof in profiles.items():
|
|
hits = {q: prof[q] == est[q][0] for q in asked}
|
|
rows.append({"name": name, "matched": sum(hits.values()), "asked": len(asked), "hits": hits, "profile": prof})
|
|
rows.sort(key=lambda r: -r["matched"])
|
|
return rows
|
|
|
|
|
|
def mismatches(answers, prof):
|
|
"""Questions whose answer differs from the template (strings compared; 'rebar_*' = '아니오' counts); empty = same form."""
|
|
out = [label for q, label in QUESTIONS if str(prof[q]) != str(answers.get(q, "")).strip()]
|
|
out += [label for key, label in (("rebar_shape", "철근 형상 종류"), ("rebar_method", "철근 생성 방식")) if answers.get(key) == "아니오"]
|
|
return out
|