- 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>
597 lines
31 KiB
Python
597 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""산외교(울산) P1 철근재료표 대조 (운곡교 make_bom_compare.py 를 산외교 도면·모델용으로 옮긴 것).
|
||
|
||
사용: python -I make_bom_compare_sw.py <pylib경로> <texts.json> <출력xlsx>
|
||
texts.json : 도면 DXF 의 글자 목록 (x, y, 레이어, 글자, 높이) - 같은 폴더 '추출데이터\도면_글자.json'
|
||
입력(고정): 산외교P1 G5.3dm (모델)
|
||
시트: ①도면 재료표(재현) ②모델 재료표 ③대조·비고 ④요약 ⑤비고코드설명 ⑥모델_개별철근
|
||
검토 범위: 모델↔도면. 도면 내부 불일치는 '도면내부_오류_보관.md' 에만 기록(여기 비고에는 넣지 않음).
|
||
"""
|
||
import sys, re, math, collections, json, os
|
||
sys.path.insert(0, sys.argv[1])
|
||
import rhino3dm
|
||
from openpyxl import Workbook
|
||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||
from openpyxl.utils import get_column_letter
|
||
|
||
R = json.load(open(sys.argv[2], encoding="utf-8"))
|
||
OUT = sys.argv[3]
|
||
M3DM = r"D:\★Pier 자료\심수석님 검토 파일\검토_산외교P1\산외교P1 G5.3dm"
|
||
UNIT = {"32": 6.230, "29": 5.040, "25": 3.980, "22": 3.040, "19": 2.250, "16": 1.560, "13": 0.995}
|
||
TOL = 1.0 + 1e-6
|
||
|
||
# =============================================================== 1. 도면 재료표 파싱
|
||
COLS = [("num", 97500, 98300), ("shape", 98300, 99000), ("dia", 99000, 99800), ("n", 99800, 100600),
|
||
("A", 100600, 101800), ("B", 101800, 103000), ("C", 103000, 104300), ("D", 104300, 105500),
|
||
("E", 105500, 106600), ("R", 106600, 107600), ("X1", 107600, 108300), ("X2", 108300, 108900),
|
||
("X3", 108900, 109500), ("X4", 109500, 110100), ("J", 110100, 111300), ("L", 111300, 112600),
|
||
("tot", 112600, 113700), ("unit", 113700, 114700), ("w", 114700, 115800), ("wh", 115800, 117500)]
|
||
rowsraw = collections.defaultdict(lambda: collections.defaultdict(list))
|
||
for x, y, lay, t, h in R:
|
||
if not lay.startswith("CS-TABL") or not (-278800 < y < -265300):
|
||
continue
|
||
tab = "SD400" if y > -274000 else "SD500"
|
||
yy = round(y / 10) * 10
|
||
t = t.strip().replace("%%%", "%").replace("%%d", "°")
|
||
if 104000 <= x < 106000 and ("소계" in t or "총계" in t):
|
||
rowsraw[(tab, yy)]["label"].append(t)
|
||
continue
|
||
for name, lo, hi in COLS:
|
||
if lo <= x < hi:
|
||
rowsraw[(tab, yy)][name].append((x, t))
|
||
break
|
||
|
||
def first(c, k):
|
||
v = c.get(k)
|
||
return sorted(v)[0][1] if v else ""
|
||
|
||
drow = {"SD400": [], "SD500": []}
|
||
for (tab, y), c in sorted(rowsraw.items(), key=lambda kv: (kv[0][0], -kv[0][1])):
|
||
lab = " ".join(c.get("label", []))
|
||
num = first(c, "num")
|
||
if lab:
|
||
drow[tab].append({"kind": "sum", "label": lab, "cells": {k: first(c, k) for k in ("tot", "unit", "w", "wh")}})
|
||
elif re.match(r"^[A-Z]{1,2}\d+$", num):
|
||
d = {k: first(c, k) for k, _, _ in COLS}
|
||
d["shape_v"] = d["shape"]; d["dia_v"] = d["dia"]
|
||
drow[tab].append({"kind": "bar", "mark": num, "cells": d, "dia_n": re.sub(r"\D", "", d["dia"])})
|
||
DR = {r["mark"]: r for tab in drow for r in drow[tab] if r["kind"] == "bar"}
|
||
|
||
def mm(s):
|
||
"""도면 표기 → mm 리스트 (범위는 큰 값~작은 값)"""
|
||
if s is None or s == "" or s == '"':
|
||
return None
|
||
s = s.replace("°", "")
|
||
out = []
|
||
for part in s.split("~"):
|
||
try:
|
||
v = float(part)
|
||
except ValueError:
|
||
return None
|
||
out.append(v * 1000.0 if ("." in part and abs(v) < 100) else v)
|
||
return out
|
||
|
||
# =============================================================== 2. 모델 읽기 / 치수 추출
|
||
m3 = rhino3dm.File3dm.Read(M3DM)
|
||
lay = {l.Index: l.FullPath for l in m3.Layers}
|
||
|
||
def V(p): return (p.X, p.Y, p.Z)
|
||
def sub(a, b): return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
|
||
def dot(a, b): return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||
def nrm(a): return math.sqrt(dot(a, a))
|
||
def addv(a, b, t): return (a[0] + b[0] * t, a[1] + b[1] * t, a[2] + b[2] * t)
|
||
def isect(p1, d1, p2, d2):
|
||
w = sub(p1, p2); a = dot(d1, d1); b = dot(d1, d2); c = dot(d2, d2); d = dot(d1, w); e = dot(d2, w)
|
||
den = a * c - b * b
|
||
if abs(den) < 1e-9:
|
||
return None
|
||
return addv(p1, d1, (b * e - c * d) / den)
|
||
def segs_of(g):
|
||
if isinstance(g, rhino3dm.PolyCurve):
|
||
return [g.SegmentCurve(i) for i in range(g.SegmentCount)]
|
||
return [g]
|
||
def is_line(s): return type(s).__name__ in ("LineCurve", "PolylineCurve")
|
||
def is_arc(s): return type(s).__name__ == "ArcCurve"
|
||
def clen(c, n=1500):
|
||
d = c.Domain; p0 = c.PointAt(d.T0); s = 0.0
|
||
for i in range(1, n + 1):
|
||
p = c.PointAt(d.T0 + (d.T1 - d.T0) * i / n)
|
||
s += nrm(sub(V(p), V(p0))); p0 = p
|
||
return s
|
||
def arc_len(s): return s.Arc.Radius * math.radians(s.Arc.AngleDegrees)
|
||
def num(s):
|
||
try: return float(s)
|
||
except Exception: return 0.0
|
||
|
||
def lines_of(g):
|
||
return [(V(s.PointAtStart), V(s.PointAtEnd)) for s in segs_of(g) if is_line(s)]
|
||
def corner_seq(g):
|
||
"""직선 구간을 연장한 꼭짓점 사이 거리(각진 치수)."""
|
||
lines = lines_of(g)
|
||
if len(lines) < 2:
|
||
return [nrm(sub(l[1], l[0])) for l in lines]
|
||
pts = []
|
||
for i in range(len(lines) - 1):
|
||
p1, q1 = lines[i]; p2, q2 = lines[i + 1]
|
||
pts.append(isect(p1, sub(q1, p1), p2, sub(q2, p2)))
|
||
seq = [lines[0][0]] + pts + [lines[-1][1]]
|
||
return [nrm(sub(seq[i + 1], seq[i])) for i in range(len(seq) - 1)]
|
||
|
||
def bar_dims(mk, shape, us, g):
|
||
"""한 철근의 재료표 항목(A..E, R, X1~X4) 을 mm / 도(°) 로. 모델에서 정의할 수 없는 항목(J 등)은 넣지 않는다."""
|
||
d = {}
|
||
segs = segs_of(g)
|
||
arcs = [s for s in segs if is_arc(s)]
|
||
seq = corner_seq(g)
|
||
if shape == "I":
|
||
if len(seq) == 1:
|
||
d["A"] = seq[0]
|
||
elif len(seq) == 3:
|
||
d["A"] = seq[1]; d["B"] = seq[0]
|
||
elif shape == "II": # C7~C9
|
||
d["C"] = seq[0]; d["B"] = seq[1]; d["A"] = seq[2]
|
||
elif shape == "III":
|
||
if mk == "C10": # 폐합 스터럽(변 2개씩)
|
||
d["B"] = seq[1]; d["A"] = seq[2]
|
||
else: # B9·B10
|
||
d["B"] = seq[0]; d["A"] = seq[1]
|
||
elif shape in ("VII", "VIII"): # C11·C13 갈고리 달린 수평근
|
||
ls = [s for s in segs if is_line(s)]
|
||
d["C"] = nrm(sub(V(ls[0].PointAtEnd), V(ls[0].PointAtStart)))
|
||
d["A"] = seq[2]
|
||
d["R"] = arcs[0].Arc.Radius - num("0")
|
||
elif shape == "XII": # C12 (R400 굽힘이 있어 도면 치수는 접선 직선 길이)
|
||
ln = [nrm(sub(l[1], l[0])) for l in lines_of(g)]
|
||
d["B"] = ln[1]; d["A"] = ln[2]
|
||
d["C"] = arc_len(arcs[0]); d["R"] = arcs[0].Arc.Radius
|
||
elif shape == "IX": # C1~C4
|
||
d["B"] = seq[0]; d["A"] = seq[1]; d["C"] = seq[2]
|
||
d["X1"] = 180.0 - arcs[0].Arc.AngleDegrees; d["X2"] = 180.0 - arcs[1].Arc.AngleDegrees
|
||
elif shape == "IV":
|
||
if mk in ("C5", "C6"):
|
||
d["A"], d["B"], d["C"], d["D"], d["E"] = seq[0], seq[1], seq[2], seq[3], seq[4]
|
||
for i, a in enumerate(arcs[:4]):
|
||
d["X%d" % (i + 1)] = 180.0 - a.Arc.AngleDegrees
|
||
else: # B2·B5
|
||
d["B"] = seq[0]; d["A"] = seq[1]
|
||
elif shape == "V":
|
||
if mk == "B8": # 기초 둘레 폐합근: 변 길이
|
||
d["A"] = seq[1]; d["B"] = seq[2]
|
||
else: # P3 이음 링 (반원)
|
||
d["A"] = arc_len(arcs[0]); d["B"] = num(us.get("coupler_length")); d["R"] = arcs[0].Arc.Radius
|
||
elif shape == "X": # P4 링
|
||
d["A"] = arc_len(arcs[0]); d["B"] = num(us.get("coupler_length")); d["R"] = arcs[0].Arc.Radius
|
||
elif shape == "VI":
|
||
big = max(arcs, key=lambda s: s.Arc.Radius)
|
||
d["R"] = big.Arc.Radius
|
||
if mk == "P1":
|
||
ls = [s for s in segs if is_line(s)]
|
||
lens = sorted(nrm(sub(V(s.PointAtEnd), V(s.PointAtStart))) for s in ls)
|
||
d["C"] = lens[0]; d["A"] = lens[-1]; d["B"] = arc_len(arcs[0])
|
||
return d
|
||
|
||
bars = collections.defaultdict(dict)
|
||
for o in m3.Objects:
|
||
L = lay.get(o.Attributes.LayerIndex, "")
|
||
g = o.Geometry; nm = o.Attributes.Name or ""
|
||
if isinstance(g, rhino3dm.Brep):
|
||
us = dict(o.Attributes.GetUserStrings())
|
||
if us.get("mark"):
|
||
bars[us["mark"]].setdefault(us.get("occurrence"), {}).update(us=us, name=nm, layer=L)
|
||
elif nm.startswith("P1-RC-"):
|
||
p = nm.split("-")
|
||
bars[p[2]].setdefault("-".join(p[3:]), {})["g"] = g
|
||
|
||
mod = {}
|
||
HALF = ("P2", "P3")
|
||
for mk, dd in bars.items():
|
||
items = [b for b in dd.values() if "us" in b and "g" in b]
|
||
if not items:
|
||
continue
|
||
shape = items[0]["us"].get("shape")
|
||
Ls = []; dims = collections.defaultdict(list); cl = 0.0
|
||
for b in items:
|
||
us = b["us"]
|
||
Ls.append(num(us.get("design_length")) + num(us.get("coupler_length")))
|
||
cl += clen(b["g"], 800)
|
||
for k, v in bar_dims(mk, shape, us, b["g"]).items():
|
||
if v is not None:
|
||
dims[k].append(v)
|
||
half = mk in HALF
|
||
n = len(items) // 2 if half else len(items)
|
||
# 형상 길이 합(곡선 실길이) 과 design_length 의 차
|
||
mod[mk] = dict(shape=shape, dia=items[0]["us"].get("dia"), n=n, pieces=len(items), tot=sum(Ls) / 1000.0,
|
||
L=sum(Ls) / n, Lmin=min(Ls), Lmax=max(Ls), dims=dims, cl=cl / 1000.0, items=items, half=half)
|
||
|
||
# =============================================================== 3. 표기·비교 도구
|
||
def fmt_one(v, key=None):
|
||
if key in ("X1", "X2", "X3", "X4"):
|
||
return "%d°" % round(v)
|
||
return "%.3f" % (v / 1000.0) if v >= 1000 else "%d" % round(v)
|
||
|
||
def agg(vals, key):
|
||
if not vals:
|
||
return "", None
|
||
if key in ("X1", "X2", "X3", "X4"):
|
||
vals = [round(v) for v in vals]
|
||
lo, hi = min(vals), max(vals)
|
||
if hi - lo <= 0.6:
|
||
return fmt_one(vals[0], key), [vals[0]]
|
||
return fmt_one(hi, key) + "~" + fmt_one(lo, key), [hi, lo]
|
||
|
||
def same(dtxt, mvals, key):
|
||
dv = mm(dtxt) if dtxt else None
|
||
if dv is None and mvals is None:
|
||
return "both_blank"
|
||
if dv is None:
|
||
return "drawing_blank"
|
||
if mvals is None:
|
||
return "model_blank"
|
||
if key in ("X1", "X2", "X3", "X4"):
|
||
return "same" if [round(x) for x in dv] == [round(x) for x in mvals] else "diff"
|
||
if len(dv) != len(mvals):
|
||
return "diff"
|
||
return "same" if all(abs(a - b) <= TOL for a, b in zip(dv, mvals)) else "diff"
|
||
|
||
DIMK = ["A", "B", "C", "D", "E", "R", "X1", "X2", "X3", "X4", "J"]
|
||
# 재료표 치수 = 모델(작도) 치수 + 가산값 (주철근D+스터럽D 등 도면 산식). 일치하면 'rule' 로 표시
|
||
RULE = {"C10": {"A": 54, "B": 99}, "C12": {"A": 54, "B": 99}, "C11": {"A": 44}, "C13": {"A": 44}}
|
||
|
||
def model_cells(mk):
|
||
m = mod.get(mk)
|
||
cells = {}; vals = {}
|
||
if not m:
|
||
return cells, vals
|
||
for k in DIMK:
|
||
t, v = agg(m["dims"].get(k, []), k)
|
||
cells[k] = t; vals[k] = v
|
||
return cells, vals
|
||
|
||
# =============================================================== 4. 비고 (모델↔도면 코드) — 도면 내부 불일치는 넣지 않음
|
||
NOTE = {}
|
||
KDS = {}
|
||
for mk in ("C1", "C2", "C3", "C4"):
|
||
NOTE[mk] = "[M-1] 일부 철근 A 가 도면 단일값보다 짧음(모델은 철근마다 개별 길이). [M-2] J 1.560 은 재료표 L 에만 포함되고 모델 형상에는 작도되지 않음."
|
||
for mk in ("C5", "C6"):
|
||
NOTE[mk] = "[M-1] 일부 철근 B·D 가 도면 4.018/4.016 보다 짧음. [M-2] J 940 은 재료표 L 에만 포함, 모델 형상에는 없음."
|
||
for mk in ("C7", "C8", "C9"):
|
||
NOTE[mk] = "[M-3] 재료표 A·C·L 은 대푯값(a=평균+54), 모델은 철근마다 작도값 → 철근별로 다름(길이 합은 근사). [M-2] 재료표 L=2A+2B+2C 의 C 660 이 모델 폐합 스터럽 형상에는 없음."
|
||
NOTE["C10"] = "[M-3] 재료표 A·B 는 모델 작도값 +54/+99. [M-4] 재료표 A 최대 9.570 에 대응하는 철근이 모델에 없음(모델 최대 A 8.633+54=8.687, 설계길이 합 -6.2 m). [M-2] J 660 은 L 에만 반영."
|
||
NOTE["C11"] = "[M-3] 재료표 A = 모델 작도값 +44 (일치). B 165 는 모델에서 정의 불가(미추출)."
|
||
NOTE["C12"] = "[M-3] 재료표 A·B = 모델 작도값 +54/+99 (일치). [M-2] J 660 은 L 에만 반영."
|
||
NOTE["C13"] = "[M-3] 재료표 A = 모델 작도값 +44 (일치). B 165 미추출. [M-2] J 660 은 L 에만 반영."
|
||
NOTE["C14"] = "[M-1] 재료표 A 3.000 단일 ↔ 모델 일부 철근 A 2.633~2.892(짧음)."
|
||
NOTE["C15"] = "[M-1] 재료표 A 4.500 단일 ↔ 모델 A 4.118/4.489/4.517."
|
||
NOTE["P2"] = "[M-2] A·B·J 는 모델에서 정의 불가(미추출). 모델은 반링 2조각(140개)=1본 → 개수 70 으로 환산."
|
||
NOTE["P3"] = "모델은 반원 2조각(24개)=1링 → 개수 12 로 환산."
|
||
NOTE["P1"] = "[M-6] 도면 표의 주철근 커플러 476개(H32)가 모델에 없음 — 모델 P1 은 커플러 없이 연속 28.545 m 로 작도(68본×7 = 476 로 추정)."
|
||
NOTE["B2"] = "[M-2] J 1.000 은 L 에만 반영, 모델 형상에는 없음(모델 중심선 11.758 < L 12.800)."
|
||
NOTE["B5"] = NOTE["B2"]
|
||
NOTE["B8"] = "[M-5] 모델은 정사각 7.345×7.345, 재료표 A 7.300 · B 7.350. [M-2] J 570 은 L 에만 반영(모델 29.339 < L 31.581)."
|
||
KDS["P5"] = "[M-7] H19 굽힘 내면지름 66.5 mm < 공칭(19.1) 3.5db 66.85 mm (0.35 mm 부족)."
|
||
for mk in ("B2", "B5", "C7", "C8", "C9", "C10", "C15"):
|
||
KDS[mk] = "[M-7] H22 굽힘 내면지름 76.8 mm < 공칭(22.2) 3.5db 77.7 mm (0.9 mm 부족)."
|
||
NOTE["P3"] = NOTE["P3"] + " [M-8] 이음(커플러 55) 각도 90°/270° 는 도면에 기준이 없어 모델 가정."
|
||
NOTE["P4"] = "[M-8] 커플러(55) 각도 90° 는 도면에 기준이 없어 모델 가정."
|
||
|
||
# =============================================================== 5. 표 행 만들기
|
||
HEAD = ["번 호", "형 식", "직 경", "개 수", "A", "B", "C", "D", "E", "R", "X1(º)", "X2(º)", "X3(º)", "X4(º)", "J", "길 이(M)", "총길이(M)", "단위중량(KG/M)", "총중량(TON)", "총중량(할증)(TON)"]
|
||
KEYS = ["num", "shape", "dia", "n", "A", "B", "C", "D", "E", "R", "X1", "X2", "X3", "X4", "J", "L", "tot", "unit", "w", "wh"]
|
||
NK = len(KEYS)
|
||
|
||
def surch(d): return 1.06 if int(d) >= 22 else 1.03
|
||
|
||
def build_model_table():
|
||
out = {}
|
||
for tab in ("SD400", "SD500"):
|
||
rows = []; grp = []
|
||
for r in drow[tab]:
|
||
if r["kind"] == "bar":
|
||
mk = r["mark"]; m = mod.get(mk)
|
||
cells = {"num": mk}; vals = {}
|
||
shp = m["shape"] if m else r["cells"]["shape_v"]
|
||
dia = m["dia"] if m else r["cells"]["dia_v"]
|
||
cells["shape"] = shp; cells["dia"] = dia
|
||
if m:
|
||
cells["n"] = str(m["n"]); vals["n"] = m["n"]
|
||
c, v = model_cells(mk)
|
||
for k in DIMK:
|
||
cells[k] = c.get(k, ""); vals[k] = v.get(k)
|
||
cells["L"] = "%.3f" % (m["L"] / 1000.0); vals["L"] = m["L"]
|
||
cells["tot"] = "%.3f" % m["tot"]; vals["tot"] = m["tot"]
|
||
else:
|
||
cells["n"] = "0"; vals["n"] = 0
|
||
cells["L"] = ""; cells["tot"] = "0.000"; vals["tot"] = 0.0
|
||
for k in DIMK:
|
||
cells[k] = ""; vals[k] = None
|
||
rows.append({"kind": "bar", "mark": mk, "cells": cells, "vals": vals, "model": m,
|
||
"dia_n": re.sub(r"\D", "", dia or ""), "dia": dia})
|
||
grp.append(rows[-1])
|
||
else:
|
||
lab = r["label"]; cells = {"num": lab}
|
||
if "소계" in lab:
|
||
tl = sum(b["vals"]["tot"] for b in grp)
|
||
dn = grp[0]["dia_n"]; u = UNIT[dn]
|
||
w = tl * u / 1000.0; wh = w * surch(dn)
|
||
pc = "(%d%%)" % round((surch(dn) - 1) * 100)
|
||
cells.update(tot="%.3f" % tl, unit="%.3f" % u, w="%.3f" % w, wh="%.3f%s" % (wh, pc))
|
||
rows.append({"kind": "sum", "label": lab, "cells": cells, "vals": {"tot": tl, "w": round(w, 3), "wh": round(wh, 3)}, "dia_n": dn})
|
||
grp = []
|
||
else:
|
||
subs = [x for x in rows if x["kind"] == "sum"]
|
||
tl = sum(s["vals"]["tot"] for s in subs)
|
||
w = sum(s["vals"]["w"] for s in subs); wh = sum(s["vals"]["wh"] for s in subs)
|
||
cells.update(tot="%.3f" % tl, w="%.3f" % w, wh="%.3f" % wh)
|
||
rows.append({"kind": "tot", "label": lab, "cells": cells, "vals": {"tot": tl, "w": w, "wh": wh}})
|
||
out[tab] = rows
|
||
return out
|
||
MT = build_model_table()
|
||
|
||
# =============================================================== 6. 대조
|
||
def compare_row(dr, mr):
|
||
res = {}; auto = []
|
||
dc = dr["cells"]; mv = mr["vals"]; m = mr["model"]
|
||
dn = int(dc["n"]) if dc["n"].isdigit() else None
|
||
if dn is None:
|
||
res["n"] = "drawing_blank"; auto.append("개수: 도면 빈칸 → 모델 %d" % mv["n"])
|
||
elif dn != mv["n"]:
|
||
res["n"] = "diff"; auto.append("개수: 도면 %d → 모델 %d (%+d)" % (dn, mv["n"], mv["n"] - dn))
|
||
else:
|
||
res["n"] = "same"
|
||
for k, mkey in (("shape", "shape"), ("dia", "dia")):
|
||
dv = dc[k + "_v"]; mvv = m[mkey] if m else None
|
||
res[k] = "same" if (mvv is None or dv == mvv) else "diff"
|
||
if mvv is not None and dv != mvv:
|
||
auto.append("%s: 도면 %s → 모델 %s" % ("형식" if k == "shape" else "직경", dv, mvv))
|
||
for k in DIMK:
|
||
if k == "J":
|
||
# J 는 모델 형상에서 정의되지 않음 (L 에만 반영)
|
||
res[k] = "model_blank" if dc.get("J") else "both_blank"
|
||
if dc.get("J"):
|
||
auto.append("J: 도면 %s → 모델 미작도(L 에만 반영)" % dc["J"])
|
||
continue
|
||
r_ = same(dc.get(k, ""), mv.get(k), k)
|
||
off = RULE.get(dr["mark"], {}).get(k)
|
||
if r_ == "diff" and off and mv.get(k):
|
||
r2 = same(dc.get(k, ""), [v + off for v in mv[k]], k)
|
||
if r2 == "same":
|
||
r_ = "rule"
|
||
auto.append("%s: 도면 %s = 모델 %s + %d (가산 규칙)" % (k, dc.get(k), mr["cells"][k], off))
|
||
res[k] = r_
|
||
if r_ == "rule":
|
||
pass
|
||
elif r_ == "diff":
|
||
auto.append("%s: 도면 %s → 모델 %s" % (k, dc.get(k), mr["cells"][k]))
|
||
elif r_ == "drawing_blank":
|
||
auto.append("%s: 도면 빈칸 → 모델 %s" % (k, mr["cells"][k]))
|
||
elif r_ == "model_blank":
|
||
auto.append("%s: 도면 %s → 모델 미추출" % (k, dc.get(k)))
|
||
dL = mm(dc["L"]); dL = dL[0] if dL else None
|
||
if m and dL is not None:
|
||
d = m["L"] - dL
|
||
if abs(d) <= TOL:
|
||
res["L"] = "same" if abs(d) < 0.5 else "round"
|
||
else:
|
||
res["L"] = "diff"; auto.append("길이 L(설계길이 평균): 도면 %.3f → 모델 %.3f m (%+.3f)" % (dL / 1000, m["L"] / 1000, d / 1000))
|
||
else:
|
||
res["L"] = "same" if dL is None and not m else "diff"
|
||
dT = float(dc["tot"]) if dc["tot"] else None
|
||
mT = mv["tot"]
|
||
if dT is not None:
|
||
d = mT - dT
|
||
tolt = max(dn or 0, mv["n"]) * TOL / 1000.0
|
||
if abs(d) <= 0.0005:
|
||
res["tot"] = "same"
|
||
elif abs(d) <= tolt and res["L"] in ("same", "round"):
|
||
res["tot"] = "round"
|
||
else:
|
||
res["tot"] = "diff"; auto.append("총길이(설계길이 합): 도면 %.3f → 모델 %.3f m (%+.3f)" % (dT, mT, d))
|
||
else:
|
||
res["tot"] = "same"
|
||
# 형상 실길이(중심선) 대비: 재료표 L 은 각진 치수 합이라 굽힘 때문에 약간 짧은 것은 정상. 큰 차이(>3%)만 기록
|
||
if m and dL is not None and dn:
|
||
clm = m["cl"] * 1000.0 / m["n"]
|
||
if dL - clm > 0.03 * dL:
|
||
auto.append("형상 중심선 평균 %.3f m < 재료표 L %.3f m (%.0f mm 짧음, 굽힘 감소 제외 시 J 등 미작도 의심)" % (clm / 1000, dL / 1000, dL - clm))
|
||
return res, auto
|
||
|
||
# =============================================================== 7. 엑셀
|
||
thin = Side(style="thin", color="999999")
|
||
BR = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||
HF = PatternFill("solid", fgColor="D9D9D9")
|
||
GREY = PatternFill("solid", fgColor="EDEDED")
|
||
ORANGE = PatternFill("solid", fgColor="F8CBAD")
|
||
BLUE = PatternFill("solid", fgColor="DDEBF7")
|
||
YEL = PatternFill("solid", fgColor="FFF2CC")
|
||
GREEN = PatternFill("solid", fgColor="E2F0D9")
|
||
FILL = {"diff": ORANGE, "drawing_blank": YEL, "model_blank": YEL, "round": BLUE, "rule": BLUE}
|
||
wb = Workbook()
|
||
VCOL = NK + 1 # 판정 열 번호
|
||
|
||
def write_table(ws, title, tables, cmp_mode=False):
|
||
ws.append([title]); ws.cell(row=1, column=1).font = Font(bold=True, size=12)
|
||
ws.append([])
|
||
heads = HEAD + (["판정", "비고 (자동: 칸별 차이)", "비고 (도면↔모델 코드)", "비고 3 (KDS 24 14 21 기준 상이)"] if cmp_mode else [])
|
||
for tab_title, rows in tables:
|
||
ws.append([tab_title]); ws.cell(row=ws.max_row, column=1).font = Font(bold=True)
|
||
ws.append(heads); hr = ws.max_row
|
||
for c in range(1, len(heads) + 1):
|
||
cell = ws.cell(row=hr, column=c); cell.font = Font(bold=True); cell.fill = HF; cell.border = BR
|
||
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||
for rr in rows:
|
||
ws.append(rr["line"]); r_ = ws.max_row
|
||
for c in range(1, len(heads) + 1):
|
||
cell = ws.cell(row=r_, column=c); cell.border = BR
|
||
cell.alignment = Alignment(vertical="top", wrap_text=(c > NK), horizontal=("center" if c <= NK else "left"))
|
||
for col, fill in rr.get("fills", {}).items():
|
||
ws.cell(row=r_, column=col).fill = fill
|
||
if rr.get("bold"):
|
||
for c in range(1, len(heads) + 1):
|
||
ws.cell(row=r_, column=c).font = Font(bold=True)
|
||
if c not in rr.get("fills", {}):
|
||
ws.cell(row=r_, column=c).fill = GREY
|
||
if cmp_mode:
|
||
ws.cell(row=r_, column=NK + 4).font = Font(color="FF00FF")
|
||
ws.append([])
|
||
widths = [8, 6, 6, 6, 10, 10, 10, 7, 7, 8, 7, 7, 7, 7, 8, 10, 11, 11, 10, 12]
|
||
if cmp_mode:
|
||
widths += [11, 60, 70, 52]
|
||
for i, w in enumerate(widths, 1):
|
||
ws.column_dimensions[get_column_letter(i)].width = w
|
||
ws.freeze_panes = "B4"
|
||
|
||
def line_of(cells):
|
||
return [cells.get(k, "") for k in KEYS]
|
||
|
||
w1 = wb.active; w1.title = "①도면 재료표(재현)"
|
||
tabs1 = []
|
||
for tab in ("SD400", "SD500"):
|
||
rows = []
|
||
for r in drow[tab]:
|
||
if r["kind"] == "bar":
|
||
c = dict(r["cells"]); c["num"] = r["mark"]
|
||
rows.append({"line": line_of(c)})
|
||
else:
|
||
c = dict(r["cells"]); c["num"] = r["label"]
|
||
rows.append({"line": line_of(c), "bold": True})
|
||
tabs1.append(("철 근 재 료 표 (%s) — 도면 원본 값 그대로" % tab, rows))
|
||
write_table(w1, "① 도면 재료표 재현 (산외교 울산 P1, 교각배근도(울산P1)_2016.10.28)", tabs1)
|
||
|
||
w2 = wb.create_sheet("②모델 재료표")
|
||
tabs2 = []
|
||
for tab in ("SD400", "SD500"):
|
||
rows = []
|
||
for r in MT[tab]:
|
||
c = dict(r["cells"])
|
||
if r["kind"] == "bar":
|
||
rows.append({"line": line_of(c)})
|
||
else:
|
||
c["num"] = r["label"]
|
||
rows.append({"line": line_of(c), "bold": True})
|
||
tabs2.append(("철 근 재 료 표 (%s) — 모델(산외교P1 G5.3dm) 기준" % tab, rows))
|
||
write_table(w2, "② 모델 재료표 (모델 철근에서 추출: 개수·형상·치수·길이·총길이, 중량은 도면과 같은 단위중량·할증 적용)", tabs2)
|
||
|
||
w3 = wb.create_sheet("③대조·비고")
|
||
tabs3 = []
|
||
EXPORT = {"SD400": [], "SD500": []}
|
||
stat = []
|
||
for tab in ("SD400", "SD500"):
|
||
rows = []
|
||
di = 0
|
||
for r in drow[tab]:
|
||
mr = MT[tab][di]; di += 1
|
||
if r["kind"] == "bar":
|
||
res, auto = compare_row(r, mr)
|
||
cells = dict(mr["cells"]); fills = {}; line = []
|
||
for ci, k in enumerate(KEYS, 1):
|
||
txt = cells.get(k, "")
|
||
if (k in res and res[k] not in ("same", "both_blank") and k not in ("shape", "dia")) or (k in ("shape", "dia") and res.get(k) == "diff"):
|
||
dt = r["cells"].get(k + "_v", r["cells"].get(k, "")) if k in ("shape", "dia") else r["cells"].get(k, "")
|
||
txt = "%s\n(도면: %s)" % (txt if txt != "" else "-", dt if dt != "" else "빈칸")
|
||
fills[ci] = FILL.get(res[k], ORANGE)
|
||
line.append(txt)
|
||
kind = list(res.values())
|
||
if "diff" in kind:
|
||
verdict = "차이"
|
||
elif "drawing_blank" in kind or "model_blank" in kind:
|
||
verdict = "도면 빈칸/모델 미추출"
|
||
elif "round" in kind or "rule" in kind:
|
||
verdict = "일치(반올림)"
|
||
else:
|
||
verdict = "일치"
|
||
fills[VCOL] = {"차이": ORANGE, "도면 빈칸/모델 미추출": YEL, "일치(반올림)": BLUE, "일치": GREEN}[verdict]
|
||
line += [verdict, "\n".join(auto), NOTE.get(r["mark"], ""), KDS.get(r["mark"], "")]
|
||
rows.append({"line": line, "fills": fills})
|
||
EXPORT[tab].append({"kind": "bar", "mark": r["mark"], "d": {k: r["cells"].get(k, "") for k in KEYS},
|
||
"m": {k: mr["cells"].get(k, "") for k in KEYS}, "res": res, "verdict": verdict,
|
||
"auto": auto, "note": NOTE.get(r["mark"], ""), "kds": KDS.get(r["mark"], "")})
|
||
stat.append((r["mark"], verdict, auto))
|
||
else:
|
||
cells = dict(mr["cells"]); cells["num"] = r["label"]; fills = {}; line = []
|
||
for ci, k in enumerate(KEYS, 1):
|
||
txt = cells.get(k, ""); dt = r["cells"].get(k, "")
|
||
if k in ("tot", "w", "wh") and dt and txt:
|
||
mt_ = re.match(r"[\d.]+", txt); md_ = re.match(r"[\d.]+", dt)
|
||
if mt_ and md_ and abs(float(mt_.group()) - float(md_.group())) > 0.0015:
|
||
txt = "%s\n(도면: %s)" % (txt, dt); fills[ci] = ORANGE
|
||
line.append(txt)
|
||
line += ["", "", "", ""]
|
||
rows.append({"line": line, "fills": fills, "bold": True})
|
||
EXPORT[tab].append({"kind": mr["kind"], "label": r["label"], "d": {k: r["cells"].get(k, "") for k in KEYS},
|
||
"m": {k: mr["cells"].get(k, "") for k in KEYS}, "res": {KEYS[ci - 1]: "diff" for ci in fills},
|
||
"verdict": "", "auto": [], "note": "", "kds": ""})
|
||
tabs3.append(("%s — 모델 값 표시, 도면과 다른 칸은 색 + (도면: 값)" % tab, rows))
|
||
write_table(w3, "③ 대조·비고 — 도면 표 순서로, 모델 값 위에 도면과 다른 칸을 표시", tabs3, cmp_mode=True)
|
||
for row in w3.iter_rows(min_row=4):
|
||
lines = 1
|
||
for c in row:
|
||
if c.value is None:
|
||
continue
|
||
s = str(c.value); wd = w3.column_dimensions[get_column_letter(c.column)].width or 10
|
||
n = sum(max(1, int(len(p) * 1.7 / wd) + 1) for p in s.split("\n")) if c.column > NK else len(s.split("\n"))
|
||
lines = max(lines, n)
|
||
w3.row_dimensions[row[0].row].height = max(15, 14 * lines)
|
||
w3.insert_rows(2)
|
||
w3.cell(row=2, column=1, value="색: 주황=도면과 다름 · 파랑=반올림(≤1 mm/본) · 노랑=모델 미작도/미추출 · 판정 초록=일치 · 마젠타 글자=KDS 24 14 21 기준 상이. 코드 [M-n]=도면↔모델(⑤ 참고). 도면 내부 불일치는 별도 보관(도면내부_오류_보관.md).")
|
||
w3.cell(row=2, column=1).font = Font(italic=True)
|
||
|
||
w4 = wb.create_sheet("④요약")
|
||
w4.append(["도면 재료표 ↔ 모델 재료표 차이 요약 (검토 미확정, 자동 비교)"]); w4.cell(row=1, column=1).font = Font(bold=True, size=12)
|
||
w4.append([]); w4.append(["마크", "판정", "차이 내용(자동 추출)"])
|
||
for c in range(1, 4):
|
||
w4.cell(row=3, column=c).font = Font(bold=True); w4.cell(row=3, column=c).fill = HF
|
||
for mk, v, auto in stat:
|
||
if v != "일치":
|
||
w4.append([mk, v, " / ".join(auto)])
|
||
w4.append([])
|
||
cnt = collections.Counter(v for _, v, _ in stat)
|
||
w4.append(["판정 집계: " + ", ".join("%s %d" % kv for kv in sorted(cnt.items()))])
|
||
for i, w in enumerate([10, 22, 160], 1):
|
||
w4.column_dimensions[get_column_letter(i)].width = w
|
||
|
||
CODES = [
|
||
("M-1", "도면↔모델", "C1~C6·C14·C15 일부 철근이 재료표 단일값보다 짧음(C1 A 8.462~9.259, C5 B 3.580~4.018, C14 A 2.633~3.000, C15 A 4.118~4.517) — 모델은 철근마다 개별 길이"),
|
||
("M-2", "도면↔모델", "J·C 길이(C1~C4 J 1.560, C5·C6 J 940, C7~C9 C 660×2, C10·C12·C13 J 660, P2 J 380, B2·B5 J 1.000, B8 J 570)가 재료표 L 에만 있고 모델 형상에는 작도되지 않음"),
|
||
("M-3", "도면↔모델", "C7~C13 치수 산식(+54/+44/+99 가산): 재료표는 대푯값·가산값, 모델은 철근마다 작도값"),
|
||
("M-4", "도면↔모델", "C10 재료표 A 최대 9.570 에 대응하는 철근이 모델에 없음(모델 A 최대 8.687)"),
|
||
("M-5", "도면↔모델", "B8 모델 정사각 7.345×7.345 ↔ 재료표 A 7.300·B 7.350"),
|
||
("M-6", "도면↔모델", "주철근 커플러 476개(H32, 도면 표)가 모델에 없음 — P1 은 연속 철근으로 작도"),
|
||
("M-7", "기준↔모델", "굽힘 내면지름: H22(B2·B5·C7~C10·C15) 76.8 < 77.7, H19(P5) 66.5 < 66.85 — KDS 24 14 21 표 4.5-1 3.5db 에 0.9/0.35 mm 부족"),
|
||
("M-8", "미확인", "P3 이음·P4 커플러 각도(90°/270°) 도면에 기준 없음, 모델 가정"),
|
||
]
|
||
w5 = wb.create_sheet("⑤비고코드설명")
|
||
w5.append(["코드", "구분", "내용"])
|
||
for c in CODES:
|
||
w5.append(list(c))
|
||
for i, w in enumerate([10, 14, 120], 1):
|
||
w5.column_dimensions[get_column_letter(i)].width = w
|
||
for c in range(1, 4):
|
||
w5.cell(row=1, column=c).font = Font(bold=True); w5.cell(row=1, column=c).fill = HF
|
||
|
||
w6 = wb.create_sheet("⑥모델_개별철근")
|
||
w6.append(["마크", "직경", "형식", "발생(occurrence)", "L(설계길이+커플러, mm)", "커플러(mm)", "중심선 실길이(mm)", "호스트"])
|
||
for mk in sorted(mod, key=lambda s: (re.sub(r"\d", "", s), int(re.sub(r"\D", "", s) or 0))):
|
||
for b in sorted(mod[mk]["items"], key=lambda b: b["us"].get("occurrence", "")):
|
||
us = b["us"]
|
||
w6.append([mk, us.get("dia"), us.get("shape"), us.get("occurrence"), round(num(us.get("design_length")) + num(us.get("coupler_length")), 3),
|
||
num(us.get("coupler_length")), round(clen(b["g"], 600), 1), us.get("host")])
|
||
for c in range(1, 9):
|
||
w6.cell(row=1, column=c).font = Font(bold=True); w6.cell(row=1, column=c).fill = HF
|
||
for i, w in enumerate([8, 7, 7, 20, 22, 11, 18, 10], 1):
|
||
w6.column_dimensions[get_column_letter(i)].width = w
|
||
w6.freeze_panes = "A2"
|
||
wb.move_sheet("④요약", offset=-3)
|
||
|
||
jp = os.path.join(os.path.dirname(os.path.dirname(OUT)) if False else os.path.dirname(OUT), "추출데이터", "재료표_대조결과.json")
|
||
os.makedirs(os.path.dirname(jp), exist_ok=True)
|
||
json.dump(EXPORT, open(jp, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
||
try:
|
||
wb.save(OUT)
|
||
except PermissionError:
|
||
print("엑셀 파일이 열려 있어 저장하지 못함(JSON 만 갱신):", OUT)
|
||
print("saved", OUT)
|
||
print("판정:", dict(cnt))
|
||
for mk, v, auto in stat:
|
||
if v != "일치":
|
||
print(mk, v, " | ".join(auto))
|