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,26 @@
import ezdxf, sys
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\2.dxf'); ms=d.modelspace()
X0,X1,Y0,Y1=6000,12600,26400,30100
print('--- front view region: lines/dots by layer')
cir=defaultdict(list)
for e in ms:
if e.dxftype()=='CIRCLE':
c=e.dxf.center
if X0<c.x<X1 and Y0<c.y<Y1: cir[(round(c.y,1),e.dxf.radius)].append(round(c.x,1))
for k in sorted(cir,reverse=True): print('circle row y',k[0],'r',k[1],'n',len(cir[k]),'x',sorted(cir[k])[:3],'..',sorted(cir[k])[-2:])
H=defaultdict(list);V=defaultdict(list)
for e in ms:
if e.dxftype()=='LINE' and e.dxf.layer in('CS-RBAR-MAJR','CS-CONC-MAJR'):
a,b=e.dxf.start,e.dxf.end
if X0<min(a.x,b.x) and max(a.x,b.x)<X1 and Y0<min(a.y,b.y) and max(a.y,b.y)<Y1:
if abs(a.y-b.y)<0.6: H[(e.dxf.layer,round(a.y,1))].append((round(min(a.x,b.x),1),round(max(a.x,b.x),1)))
elif abs(a.x-b.x)<0.6: V[(e.dxf.layer,round(a.x,1))].append((round(min(a.y,b.y),1),round(max(a.y,b.y),1)))
for k in sorted(H,reverse=True): print('H',k,sorted(H[k])[:3])
for k in sorted(V): print('V',k,sorted(V[k])[:2])
print('--- texts')
for e in ms:
if e.dxftype()=='TEXT':
p=e.dxf.insert
if X0<p.x<X1 and Y0<p.y<Y1: print((round(p.x),round(p.y)),e.dxf.text)
@@ -0,0 +1,20 @@
import ezdxf, sys
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\2.dxf'); ms=d.modelspace()
X0,X1,Y0,Y1=17500,26500,26000,31600
H=defaultdict(list);V=defaultdict(list)
for e in ms:
if e.dxftype()=='LINE' and e.dxf.layer in('CS-RBAR-MAJR','CS-CONC-MAJR'):
a,b=e.dxf.start,e.dxf.end
if X0<min(a.x,b.x) and max(a.x,b.x)<X1 and Y0<min(a.y,b.y) and max(a.y,b.y)<Y1:
if abs(a.y-b.y)<0.6: H[(e.dxf.layer,round(a.y,1))].append((round(min(a.x,b.x),1),round(max(a.x,b.x),1)))
elif abs(a.x-b.x)<0.6: V[(e.dxf.layer,round(a.x,1))].append((round(min(a.y,b.y),1),round(max(a.y,b.y),1)))
print('H lines (y, x-range, count)')
for k in sorted(H,reverse=True): print(' ',k,sorted(H[k])[:2],len(H[k]))
print('V lines (x, y-range, count)')
for k in sorted(V): print(' ',k,sorted(V[k])[:2],len(V[k]))
for e in ms:
if e.dxftype()=='TEXT':
p=e.dxf.insert
if X0<p.x<X1 and Y0<p.y<Y1: print((round(p.x),round(p.y)),e.dxf.text)
@@ -0,0 +1,29 @@
import sys; sys.path.insert(0,"./pylib"); sys.stdout.reconfigure(encoding='utf-8')
import rhino3dm, numpy as np, math
m=rhino3dm.File3dm.Read('m.3dm'); byidx={l.Index:l.FullPath for l in m.Layers}
L=('P1::받침대좌','P1::받침패드','P1::교량받침','P1::솔플레이트')
res={}
for o in m.Objects:
p=byidx.get(o.Attributes.LayerIndex)
if p not in L: continue
g=o.Geometry
pts=[]
try:
for fi in range(len(g.Faces)):
k=g.Faces[fi].GetMesh(rhino3dm.MeshType.Any)
if k is None: continue
pts+= [(v.X,v.Y,v.Z) for v in k.Vertices]
except Exception as e:
bb=g.GetBoundingBox(); pts=[(bb.Min.X,bb.Min.Y,bb.Min.Z),(bb.Max.X,bb.Max.Y,bb.Max.Z)]
P=np.array(pts)
xy=P[:,:2]-P[:,:2].mean(axis=0)
# find rotation minimizing bbox area
best=None
for ang in np.arange(-10,10.01,0.05):
a=math.radians(ang); R=np.array([[math.cos(a),-math.sin(a)],[math.sin(a),math.cos(a)]])
q=xy@R.T; w=q[:,0].max()-q[:,0].min(); h=q[:,1].max()-q[:,1].min()
if best is None or w*h<best[0]: best=(w*h,ang,w,h)
res.setdefault(p,[]).append((o.Attributes.Name,round(best[2],1),round(best[3],1),round(best[1],2),round(P[:,2].min(),1),round(P[:,2].max(),1),round(P[:,0].mean()),round(P[:,1].mean())))
for p in L:
print('=====',p)
for r in sorted(res.get(p,[]),key=lambda r:r[0]): print(' ',r)
@@ -0,0 +1,16 @@
import ezdxf, sys, math
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
polys=[]
for e in ms:
if e.dxftype()=='LWPOLYLINE':
p=[(a[0],a[1]) for a in e.get_points()]
if len(p)>=4 and len(p)<=5 and all(12100<x<19000 and 25800<y<30200 for x,y in p):
xs=[x for x,y in p[:4]]; ys=[y for x,y in p[:4]]
side=[math.hypot(p[i][0]-p[(i+1)%4][0],p[i][1]-p[(i+1)%4][1]) for i in range(4)]
ang=math.degrees(math.atan2(p[1][1]-p[0][1],p[1][0]-p[0][0]))
polys.append((e.dxf.layer,round(sum(xs)/4,1),round(sum(ys)/4,1),[round(s,1) for s in side],round(ang,2)))
for e in ms:
if e.dxftype()=='LINE' and False: pass
for p in sorted(polys,key=lambda r:(r[0],r[1],r[2])): print(p)
print(len(polys))
@@ -0,0 +1,162 @@
import ezdxf, sys
sys.stdout.reconfigure(encoding='utf-8')
SRC = r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩'
OUT = r'D:\★Pier 자료\심수석님 검토 파일\검토_운곡교P1'
YEL, ORG, GRN = 2, 30, 3 # 도면 내부 / 도면↔모델 / 일치(정상 차이)
LAY = {YEL: '검토_노랑_도면내부', ORG: '검토_주황_도면모델', GRN: '검토_초록_일치'}
def prep(doc):
for c, n in LAY.items():
if n not in doc.layers:
doc.layers.add(n, color=c)
if 'KR_CHK' not in doc.styles:
doc.styles.add('KR_CHK', font='malgun.ttf')
def rect(m, c, x0, y0, x1, y1, w=0):
m.add_lwpolyline([(x0, y0), (x1, y0), (x1, y1), (x0, y1)], close=True, dxfattribs={'layer': LAY[c], 'color': c, 'const_width': w})
def txt(m, c, s, x, y, h):
t = m.add_text(s, height=h, dxfattribs={'layer': LAY[c], 'color': c, 'style': 'KR_CHK'})
t.set_placement((x, y))
def circ(m, c, x, y, r):
m.add_circle((x, y), r, dxfattribs={'layer': LAY[c], 'color': c})
def line(m, c, a, b):
m.add_line(a, b, dxfattribs={'layer': LAY[c], 'color': c})
# ======================= D1 =======================
d = ezdxf.readfile(SRC + r'\1.dxf'); prep(d); m = d.modelspace()
# -- section A-A : 4.5 mm (yellow, drawing-internal D-1) / tier dots confirmed (green)
for y, nm in ((24587.0, '1단 C1·C4'), (24487.0, '2단 C2·C5'), (24387.0, '3단 C3·C6')):
rect(m, YEL, 43070, y - 48, 46070, y + 48, 8)
txt(m, YEL, nm, 46110, y - 40, 85)
fans = {24587.0: [43382.3, 43632.3, 43882.3, 43507.3, 43757.3, 44007.3],
24487.0: [45132.3, 45382.3, 45632.3, 45257.3, 45507.3, 45757.3],
24387.0: [43120.3, 43382.3, 43632.3, 43257.3, 43507.3, 43757.3]}
for y, xs in fans.items():
for x in xs:
circ(m, GRN, x, y, 44)
line(m, YEL, (43030, 24591.5), (46110, 24591.5))
txt(m, YEL, 'D-1 치수 100 기준선 y=24591.5', 43080, 24850, 60)
txt(m, YEL, 'D-1 작도 점 y=24587.0 (+4.5mm)', 43080, 24780, 60)
# -- A-A bottom row C7/C8 : drawing-internal D-2 (110 vs 100) yellow, dots confirmed green
rect(m, YEL, 43070, 23264.7 - 48, 46070, 23264.7 + 48, 8)
txt(m, YEL, 'D-2 하면 치수 110 (B-B·정면도는 100)', 43080, 23150, 70)
# -- A-A stirrup top / bottom lines : D-3 (77.5 vs 100)
line(m, YEL, (43030, 24614.0), (46110, 24614.0))
txt(m, YEL, 'D-3 스터럽 상단선 77.5 (정면도 100)', 43080, 24710, 60)
# -- section B-B same marks (yellow)
for y in (13519.7, 13375.7, 13275.7, 13175.7, 10584.7):
rect(m, YEL, 43130, y - 48, 46340, y + 48, 8)
txt(m, YEL, 'D-1 B-B 점 +4.5mm (C16 / 1~3단 / C7·C8)', 43140, 13790, 60)
txt(m, YEL, 'D-3 B-B 스터럽 상·하 77.5 (정면도 100)', 43140, 13710, 60)
line(m, YEL, (43130, 13546.7), (46340, 13546.7))
line(m, YEL, (43130, 10557.7), (46340, 10557.7))
# -- elevation : model vs drawing (orange)
CXe = 12159.4
def yEL(el): return 19515.6 + (el - 43.411) * 1000.0
for xo, el_dr, el_md in ((1913, 41.011, 41.361), (2913, 41.511, 41.861)):
x = CXe + xo
y1, y2 = yEL(el_dr), yEL(el_md)
circ(m, ORG, x, y1, 110)
circ(m, ORG, x, y2, 110)
line(m, ORG, (x, y1 + 110), (x, y2 - 110))
txt(m, ORG, 'M-1 C14 도면 EL %.3f / 모델 EL %.3f (+350)' % (el_dr, el_md), x + 160, (y1 + y2) / 2 - 40, 85)
# mirrored position on the left half (model has the same lowest ties)
circ(m, ORG, 18248.3, 18108.4, 200)
circ(m, ORG, 13233.4, 16615.6, 200)
txt(m, ORG, 'M-3 C7·C8 굽힘 각진 연결 (반지름 없음)', 14500, 16380, 85)
txt(m, ORG, 'M-3 (좌우 4곳)', 18300, 17850, 85)
# -- legend (top right free area)
LEG = [(YEL, '[색 구분] 노랑 = 도면 내부 불일치 (도면끼리 다름)'),
(ORG, ' 주황 = 도면 ↔ 모델 불일치 (모델 확인 필요)'),
(GRN, ' 초록 = 일치 확인 (지시선이 가리키는 점 등)'),
(YEL, 'D-1 단면 점이 치수보다 4.5mm 안쪽 D-2 A-A 하면 110 ↔ B-B 100'),
(YEL, 'D-3 단면 스터럽 높이가 정면도보다 45mm 큼'),
(ORG, 'M-1 C14 최하단 점 2개 모델이 350mm 높음 M-3 C7·C8 굽힘 반지름 없음'),
(YEL, '기초 D-15 정면도·측면도 철근 높이 27mm 불일치 D-16 B12 34 ↔ 28 D-17 B3 D-18 B10'),
(ORG, '기초 M-8 모델 교차 철근 간격 54mm(도면 27) M-9 B5 피복 58.5 M-10 반지름 S25·S19 경계'),
(YEL, '기둥 D-20 P1/P2 상단 정면도 ↔ 재료표 D-21 반지름 875 ↔ 점 870.5 D-23 P4·P5 @100 ↔ @120'),
(ORG, '기둥 M-11 P3 갈고리 연장·내면지름 (KDS) M-12 이음·커플러 각도 기준 도면에 없음'),
(GRN, 'C1~C8 높이·가로 위치·본수는 단면 A-A·B-B·정면도와 모델 일치')]
for i, (c, s) in enumerate(LEG):
txt(m, c, s, 45500, 29950 - i * 95, 62)
# -- footing (기초) : views disagree by 27 mm (yellow, D-15) / model spacing 54 vs 27 (orange, M-8)
for (xa, xb, tag, tx) in ((12100, 16800, '정면도', 13500), (30100, 34000, '측면도', 31400)):
for (ya, yb) in ((10160, 10245), (8205, 8295), (8315, 8395)):
rect(m, YEL, xa, ya, xb, yb, 6)
txt(m, YEL, 'D-15 %s: 같은 철근을 점/선으로 그린 높이가 27mm 다름' % tag, tx, 11300, 80)
txt(m, YEL, ' (상 2073/2100, 중앙 227.5/250, 하 122/150)', tx, 11180, 80)
txt(m, ORG, 'M-8 모델 교차 철근 간격 54mm (도면 27mm), M-9 B5 중심 73 아래', tx, 11420, 80)
txt(m, YEL, 'D-16 B12 재료표 34 ↔ 작도 28 D-17 B3 치수 33@250+450 ↔ 38본 D-18 B10 9@200(10단) ↔ 9단', 13500, 11540, 80)
# -- column (기둥): P1/P2 top (drawing internal D-20), hoop pitch label (D-23), P3 hook (orange M-11)
rect(m, YEL, 12950, 19060, 13120, 19180, 8)
txt(m, YEL, 'D-20 P1 정면도 상단 EL 43.011 ↔ 재료표 43.061 (모델은 재료표)', 13250, 19100, 80)
rect(m, YEL, 12900, 16460, 13080, 16580, 8)
txt(m, YEL, 'D-20 P2 정면도는 기둥 상단 EL 40.411 에서 끝 ↔ 재료표 EL 43.061', 13250, 16640, 80)
txt(m, YEL, 'D-23 P4·P5 라벨 H19 @100 ↔ 치수 @120 (모델 @120, 계산서 확인)', 14900, 11820, 80)
txt(m, ORG, 'M-11 P3 갈고리 연장 86.7 < 5db(95), 내면지름 66.5 (모델)', 14900, 15330, 80)
txt(m, GRN, 'P3 36단 @300 / P5 22단 @120 / P4 9단 @120 / P6 9단 @1800 높이·단 수 모델과 일치', 14900, 13000, 80)
# -- zoom schematic of the 4.5 mm (drawing-internal D-1), x6
def M(x, y): return (46800 + (x - 43040) * 6, 27950 + (y - 24545) * 6)
rect(m, YEL, 46800, 27950, 47760, 28910, 6)
a = M(43040, 24691.5); b = M(43200, 24691.5); line(m, YEL, a, b)
a = M(43040, 24591.5); b = M(43200, 24591.5); line(m, YEL, a, b)
c = M(43120.3, 24587.0); circ(m, YEL, c[0], c[1], 27 * 6)
a = M(43070, 24691.5); b = M(43070, 24591.5); line(m, YEL, a, b)
txt(m, YEL, 'D-1 확대 x6 : 치수 100 기준선 vs 작도 점 (4.5mm)', 46800, 28940, 60)
txt(m, YEL, '치수 100', a[0] + 60, (a[1] + b[1]) / 2, 70)
t = M(43172, 24591.5); u = M(43172, 24587.0); line(m, YEL, t, u)
txt(m, YEL, '4.5mm', t[0] + 30, u[1] - 70, 90)
d.saveas(OUT + r'\1_도면검토_색구분_C1-C16.dxf')
# ======================= D3 =======================
d = ezdxf.readfile(SRC + r'\3.dxf'); prep(d); m = d.modelspace()
ROW = {'C1': 29900, 'C2': 29540, 'C3': 29180, 'C4': 28820, 'C5': 28460, 'C6': 28100, 'C7': 27740, 'C8': 27380,
'C9': 26660, 'C10': 26300, 'C11': 25940, 'C12': 25580, 'C13': 25220}
COLX = {'A': (5352, 6852), 'B': (6852, 8352), 'C': (8352, 9852), 'D': (9852, 11352), 'E': (11352, 12852), 'R': (12852, 14352),
'X1': (14352, 15852), 'X2': (15852, 17352), 'J': (17352, 18852), 'L': (18852, 20352)}
def cell(c, mark, col, w=14):
y = ROW[mark]; x0, x1 = COLX[col]
rect(m, c, x0 + 6, y - 99, x1 - 6, y + 249, w)
# green : normal difference (C1 row, 42mm)
for col in ('A', 'B', 'C', 'L'):
cell(GRN, 'C1', col, 22)
rect(m, GRN, 9749, 8662, 13765, 12346, 25)
# yellow : drawing-internal
for mk in ('C2', 'C5'):
cell(YEL, mk, 'L') # D-7 L 12.081 vs 12.082
for mk in ('C1', 'C2', 'C3', 'C4', 'C5', 'C6'):
cell(YEL, mk, 'X1'); cell(YEL, mk, 'X2') # D-8 angle 98/96
for mk in ('C7', 'C8'):
for col in ('B', 'D', 'R', 'X1', 'X2', 'J'):
cell(YEL, mk, col, 8) # D-9 blank cells
for mk in ('C9', 'C10', 'C11'):
cell(YEL, mk, 'A') # D-4 width 1.407
for mk in ('C12', 'C13'):
cell(YEL, mk, 'B') # D-5 width 1.407
for mk in ('C9', 'C11', 'C12'):
cell(YEL, mk, 'L') # D-6 L = (max+min)/2
rect(m, ORG, 1752, 24500 - 99, 25152, 24500 + 261, 18) # UA1 row : not modelled (M-13)
NOTES = [(YEL, '[색 구분] 노랑 = 도면 내부 불일치 / 주황 = 도면↔모델 불일치 / 초록 = 일치(정상 차이)'),
(ORG, 'M-13 UA1 (기초 조립용 철근 H22 73본, 377.848 m) 은 모델에 없음 (표의 UA1 행 주황 박스). 도면은 배치 예시도'),
(YEL, 'D-4 C9·C10·C11 폭 A 1.407 ↔ 단면 1.160/1.740/1.160 D-5 C12·C13 폭 B 1.407 ↔ 단면 1.106/1.686/1.106(+54.3)'),
(YEL, 'D-6 C9·C11·C12 L 이 (최대+최소)/2 근사 (정확 길이와 C9 -23.8m, C11 -21.3m, C12 -32.0m)'),
(YEL, 'D-7 C2·C5 L 12.081 ↔ A+B+C 12.082 D-8 다리각도 98°/96° ↔ 정면도 96.4° D-9 C7·C8 B·D·R·X·J 빈칸'),
(GRN, 'C1~C6 모델 중심선이 재료표 L 보다 약 42mm 짧음 = 모서리 굽힘 반경 반영 (가공도 VIII 는 각진 3변 a+b+c) → 정상 차이'),
(ORG, '도면↔모델 불일치(M-1 C14 점 높이, M-3 C7·C8 굽힘 반지름)는 D1 정면도에 주황색으로 표기')]
for i, (c, s) in enumerate(NOTES):
txt(m, c, s, 1900, 18650 - i * 250, 130)
d.saveas(OUT + r'\3_도면검토_색구분_C1-C16.dxf')
# ======================= D2 : S1 / S2 =======================
d = ezdxf.readfile(SRC + r'\2.dxf'); prep(d); m = d.modelspace()
# front view (S=1:30, coordinates are 2x real): S1 line y=28242.8, dots y=28269.8, pedestal top y=28342.8
rect(m, ORG, 7660, 26800, 12620, 28380, 14)
txt(m, ORG, 'M-6 모델: S1 을 코핑 상면 -50 (대좌 아래)에 둠 / 도면: 대좌 상면 -50 (대좌 안)', 7660, 26560, 110)
txt(m, ORG, 'M-7 모델: S1 위 · S2 아래 (50 간격) / 도면: S2 점이 S1 선 위', 7660, 26380, 110)
txt(m, GRN, '길이 2.4 / 1.1 m, 훅 700(89+611), 본수 10 / 21, 간격 (113/125/…/112, 75/125…) : 모델과 일치', 7660, 26200, 110)
txt(m, YEL, 'D-14 상세 제목은 전열인데 대좌 높이 139 (모델 전열 164 / 후열 139)', 7660, 26020, 110)
# section C-C : mesh
rect(m, GRN, 19505, 26865, 24505, 29265, 14)
txt(m, GRN, 'S1 10본 / S2 21본 간격 모델과 일치', 19505, 26660, 110)
# ELEVATION TABLE / bearing details (받침)
rect(m, GRN, 21000, 3600, 39500, 8950, 12)
txt(m, GRN, '받침 표: h1·h2·h3·h4·A·B·E·EL.X 및 솔플레이트 L·W·T·S 값이 모델 대좌·패드·받침·솔플레이트와 일치', 21000, 9950, 110)
txt(m, YEL, 'D-24 D1 단면도 전열 대좌 169.4 ↔ 표 h1 164 (모델 164) D-25 D1 평면도 후열 패드 840 ↔ 표 740 (모델 740)', 21000, 10130, 110)
txt(m, GRN, 'S1 852 + S2 1298 + S1 950 = 3100 : 받침 중심(-698 / +600)으로 코핑 폭을 나눈 값과 모델 일치', 21000, 10310, 110)
d.saveas(OUT + r'\2_도면검토_색구분_S1-S2.dxf')
print('saved')
@@ -0,0 +1,33 @@
import math, collections, sys
sys.stdout.reconfigure(encoding='utf-8')
# soffit EL(x): flat 40.411 for |x|<=1100, else arc R=12000 centre offset 6951.8 / y 6039.2 (drawing coords), EL = 43.411+(y-19515.6)/1000
def soffit(x):
ax=abs(x)
if ax<=1100: return 40.411
dx=6951.8-ax
y=6039.2+math.sqrt(12000**2-dx**2)
return 43.411+(y-19515.6)/1000
def top(x):
ax=x
for lim,el in ((-3800,43.411),(-1295,43.483),(1211,43.555),(3716,43.627),(99999,43.698)):
if ax<lim: return el
return 43.698
# model C14 ties (x, z_top EL of horizontal bar), from pcol.txt
ties=[]
for l in open('pcol.txt',encoding='utf-8'):
if l.startswith('P1-RC-C14-'):
p=l.split(' | '); mn=eval(p[2].split('min ')[1]); mx=eval(p[3].split('max ')[1])
ties.append((round(mn[0]),mx[2]/1000.0))
# drawing dots positions (from elevation) : right half x>=13
dr={13:[43.188,43.088,42.011,41.011],913:[43.188,43.088,42.511,41.511],1913:[43.188,43.088,42.011,41.011],2913:[43.442,43.188,43.088,42.511,41.511],3913:[43.188,43.088,42.011],4913:[43.188,43.088,42.511]}
print('x tie EL | hook DOWN end vs soffit (clear mm) | hook UP end vs top (clear mm)')
bad=[]
for x,els in sorted(dr.items()):
for el in els:
dn=el-0.350; up=el+0.350
c_dn=(dn-soffit(x))*1000; c_up=(top(x)-up)*1000
flag=''
if c_dn<0: flag+=' <-- 아래 훅이 하면 밖'
if c_up<0: flag+=' <-- 위 훅이 상면 밖'
if flag: print('%5d %.3f | %7.0f | %7.0f %s'%(x,el,c_dn,c_up,flag))
print('(other drawing ties: both directions fit)')
@@ -0,0 +1,17 @@
import ezdxf, sys, math
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
CXe=12159.4
def z(y): return 26208+(y-8115.6)
for e in ms:
t=e.dxftype()
if t=='LINE' and e.dxf.layer=='CS-RBAR-MAJR':
a,b=e.dxf.start,e.dxf.end
if 11900<min(a.x,b.x) and max(a.x,b.x)<13600 and 8300<min(a.y,b.y) and max(a.y,b.y)<10400 and (abs(a.x-b.x)<0.6 or abs(a.y-b.y)<0.6):
print('LINE (%.1f,%.1f)->(%.1f,%.1f) len %.1f z %.1f..%.1f x-off %.1f..%.1f'%(a.x,a.y,b.x,b.y,math.hypot(a.x-b.x,a.y-b.y),z(min(a.y,b.y)),z(max(a.y,b.y)),min(a.x,b.x)-CXe,max(a.x,b.x)-CXe))
elif t=='ARC':
c=e.dxf.center
if 11900<c.x<13600 and 8300<c.y<10400:
r=e.dxf.radius; sa,ea=e.dxf.start_angle,e.dxf.end_angle
p=lambda a:(c.x+r*math.cos(math.radians(a))-CXe,z(c.y+r*math.sin(math.radians(a))))
print('ARC r=%.1f center (%.1f,%.1f) ang %.1f..%.1f start(xoff,z)=(%.1f,%.1f) end=(%.1f,%.1f)'%(r,c.x,c.y,sa,ea,*p(sa),*p(ea)))
@@ -0,0 +1,25 @@
import ezdxf, sys
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
CXe=12159.4
def EL(y): return 26.208+(y-8115.6)/1000.0 # footing bottom 8115.6 = EL 26.208
x0,x1,y0,y1=11000,13400,10320,16520
V=defaultdict(list);H=defaultdict(list);C=defaultdict(list)
for e in ms:
t=e.dxftype()
if t=='LINE' and e.dxf.layer in('CS-RBAR-MAJR','CS-CONC-MAJR','CS-RBAR-EREC'):
a,b=e.dxf.start,e.dxf.end
if x0<min(a.x,b.x) and max(a.x,b.x)<x1 and y0<min(a.y,b.y) and max(a.y,b.y)<y1:
if abs(a.x-b.x)<0.6 and abs(a.y-b.y)>500: V[(e.dxf.layer[3:7],round(a.x-CXe,1))].append((round(min(a.y,b.y),1),round(max(a.y,b.y),1)))
elif abs(a.y-b.y)<0.6 and abs(a.x-b.x)>200: H[(e.dxf.layer[3:7],round(a.y,1))].append((round(min(a.x,b.x)-CXe),round(max(a.x,b.x)-CXe)))
elif t=='CIRCLE':
c=e.dxf.center
if x0<c.x<x1 and y0<c.y<y1: C[round(c.x-CXe,1)].append(round(c.y,1))
print('vertical lines (x offset from pier centre):')
for k in sorted(V): print(' ',k,[ (a,b,'EL %.3f~%.3f'%(EL(a),EL(b))) for a,b in sorted(V[k])][:3],len(V[k]))
print('horizontal lines >200 long:')
for k in sorted(H,reverse=True)[:25]: print(' ',k,sorted(H[k])[:2],'EL %.3f'%EL(k[1]))
print('circle columns (x offset: count, y min..max -> EL):')
for k in sorted(C):
ys=sorted(C[k]); print(' ',k,len(ys),ys[0],ys[-1],'EL %.3f..%.3f'%(EL(ys[0]),EL(ys[-1])))
@@ -0,0 +1,19 @@
import sys,collections,math
sys.stdout.reconfigure(encoding='utf-8')
by=collections.defaultdict(list); segs={}; cur=None
for l in open('pcol.txt',encoding='utf-8'):
if l.startswith(' seg'): segs.setdefault(cur,[]).append(l.strip()); continue
if ' | len ' not in l: continue
p=l.strip().split(' | '); nm=p[0]; k=nm.split('-')[2]; cur=k
d={'nm':nm,'len':float(p[1].split()[1]),'mn':eval(p[2].split('min ')[1]),'mx':eval(p[3].split('max ')[1])}
for q in p[4:]:
kk,_,vv=q.partition(' '); d[kk]=vv
by[k].append(d)
for k in sorted(by):
v=by[k]; f=v[0]
zs=sorted({round(r['mn'][2],1) for r in v}); zt=sorted({round(r['mx'][2],1) for r in v})
print('====',k,'n',len(v),'dia',f.get('dia'),'shape',f.get('shape'),'design',sorted({float(r['dl']) for r in v})[:4],'coupler',sorted({r.get('cl') for r in v})[:3],'sum %.3f'%(sum(float(r['dl']) for r in v)/1000))
print(' example',f['nm'],'| host',f.get('host'),'| anchor',f.get('anchor'),'| src:',(f.get('src') or '')[:160])
print(' Z min',zs[:6],'..',zs[-3:],'n',len(zs),' Z max',zt[:4],'..',zt[-3:])
print(' X',round(min(r['mn'][0] for r in v)),round(max(r['mx'][0] for r in v)),'Y',round(min(r['mn'][1] for r in v)),round(max(r['mx'][1] for r in v)))
for s in segs.get(k,[])[:7]: print(' ',s)
@@ -0,0 +1,29 @@
import ezdxf, sys, math
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
for name,(cx,cy) in (('C-C',(23572.5,26567.1)),('D-D',(29792.0,26567.1)),('E-E',(35702.8,26567.1))):
print('=== 단면',name)
K=defaultdict(list)
for e in ms:
t=e.dxftype()
if t=='CIRCLE':
c=e.dxf.center
dist=math.hypot(c.x-cx,c.y-cy)
if dist<1100:
K[(e.dxf.layer,round(e.dxf.radius,1))].append((round(dist,1),round(math.degrees(math.atan2(c.y-cy,c.x-cx))%360,3)))
elif t=='ARC':
c=e.dxf.center
if math.hypot(c.x-cx,c.y-cy)<1 and e.dxf.layer=='CS-RBAR-MAJR':
K[('ARC',round(e.dxf.radius,1))].append((round(e.dxf.start_angle,2),round(e.dxf.end_angle,2)))
elif t=='LINE' and e.dxf.layer=='CS-RBAR-MAJR':
a,b=e.dxf.start,e.dxf.end
if math.hypot(a.x-cx,a.y-cy)<1050 and math.hypot(b.x-cx,b.y-cy)<1050:
K[('LINE',round(math.degrees(math.atan2(b.y-a.y,b.x-a.x))%180,1))].append((round(math.hypot(b.x-a.x,b.y-a.y),1),))
for k,v in sorted(K.items(),key=str):
if k[0]=='ARC': print(' ARC R',k[1],v); continue
if k[0]=='LINE': print(' LINE angle',k[1],'len',v[:3],len(v)); continue
ds=sorted({a for a,b in v}); angs=sorted(b for a,b in v)
print(' ',k,'n',len(v),'dist',ds[:5],'angles',angs[:4],'..',angs[-3:])
if len(angs)>3 and len(angs)<=60:
diffs=sorted({round(angs[i+1]-angs[i],3) for i in range(len(angs)-1)}); print(' angle steps',diffs[:6])
@@ -0,0 +1,17 @@
import ezdxf, sys
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
CXe=12159.4
def EL(y): return 43.411+(y-19515.6)/1000.0
V=defaultdict(list); arcs=[]
for e in ms:
t=e.dxftype()
if t=='LINE' and e.dxf.layer in('CS-RBAR-MAJR','CS-RBAR-EREC'):
a,b=e.dxf.start,e.dxf.end
if abs(a.x-b.x)<0.6 and 11500<a.x<13300 and 13000<min(a.y,b.y) and max(a.y,b.y)<19900 and abs(a.y-b.y)>200:
V[round(a.x-CXe,1)].append((round(min(a.y,b.y),1),round(max(a.y,b.y),1)))
print('vertical bar lines in/under coping (x offset): y range -> EL (valid above break only: coping soffit y=16515.6 = EL 40.411)')
for k in sorted(V):
for a,b in sorted(V[k]): print(' x',k,'y',a,'~',b,' EL %.3f ~ %.3f'%(EL(a),EL(b)))
# any arcs near P1 hook at top? lines near coping top near column: horizontal short lines
@@ -0,0 +1,22 @@
import sys, collections, ezdxf
sys.stdout.reconfigure(encoding='utf-8')
CXe=12159.4
def EL(y): return 43.411+(y-19515.6)/1000.0
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
dots=set()
for e in ms:
if e.dxftype()=='CIRCLE' and e.dxf.radius==27:
c=e.dxf.center
if 12000<c.x<18700 and 14000<c.y<19900: dots.add((round(c.x-CXe),round(EL(c.y),3)))
# model C14
mod=collections.defaultdict(list)
for l in open('c9_16.txt',encoding='utf-8'):
if l.startswith('P1-RC-C14-'):
p=l.split(' | '); mn=eval(p[2].split('min ')[1]); mx=eval(p[3].split('max ')[1])
mod[round(mn[0])].append(round(mx[2]/1000.0,3))
print('elevation dots (r=27) per x offset -> EL (m):')
byx=collections.defaultdict(list)
for x,e in dots: byx[x].append(e)
for x in sorted(byx): print(' x',x,sorted(byx[x],reverse=True))
print('model C14 per x -> top EL of tie (horizontal part):')
for x in sorted(mod): print(' x',x,sorted(mod[x],reverse=True))
@@ -0,0 +1,72 @@
import sys, collections, ezdxf
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
CXe = 12159.4
def EL(y):
return 43.411 + (y - 19515.6) / 1000.0
# ---- model
rows = []
for l in open('c9_16.txt', encoding='utf-8'):
if l.startswith(' seg') or ' | len ' not in l:
continue
p = l.strip().split(' | ')
nm = p[0]
ln = float(p[1].split()[1])
mn = eval(p[2].split('min ')[1])
mx = eval(p[3].split('max ')[1])
dl = float(p[4].split()[1])
rows.append((nm, ln, mn, mx, dl))
by = collections.defaultdict(list)
for r in rows:
by[r[0].split('-')[2]].append(r)
# ---- elevation stirrups (vertical lines)
d = ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf')
ms = d.modelspace()
V = {}
for e in ms:
if e.dxftype() == 'LINE' and e.dxf.layer == 'CS-RBAR-MAJR':
a, b = e.dxf.start, e.dxf.end
if abs(a.x - b.x) < 0.6 and 11900 < a.x < 18600 and 15800 < min(a.y, b.y) and max(a.y, b.y) < 19800 and abs(a.y - b.y) > 300:
V.setdefault(round(a.x - CXe), []).append((min(a.y, b.y), max(a.y, b.y)))
print('--- stirrup centreline heights: elevation vs model (mm)')
mx = 0
cnt = 0
for k in ('C10', 'C11'):
for r in sorted([r for r in by[k] if r[0].endswith('k1')], key=lambda r: r[2][0]):
x = round(r[2][0])
cand = [v for v in V.get(x, []) if v[1] - v[0] > 1000]
if not cand:
print(k, x, 'no elevation line')
continue
y0, y1 = cand[0]
eb, et = EL(y0) * 1000, EL(y1) * 1000
db = r[2][2] - eb
dt = r[3][2] - et
mx = max(mx, abs(db), abs(dt))
cnt += 1
if abs(db) > 1.5 or abs(dt) > 1.5:
print(k, x, 'bottom model-elev', round(db, 1), 'top', round(dt, 1))
print('compared', cnt, 'stirrup positions, max |diff| =', round(mx, 2), 'mm')
# ---- elevation horizontal bars C12/C13 half lengths
H = {}
for e in ms:
if e.dxftype() == 'LINE' and e.dxf.layer == 'CS-RBAR-MAJR':
a, b = e.dxf.start, e.dxf.end
if abs(a.y - b.y) < 0.6 and 16500 < a.y < 19000 and 12100 < min(a.x, b.x) < 12200 and max(a.x, b.x) > 13000:
H[round(a.y, 1)] = max(a.x, b.x) - CXe
print('--- C12/C13 loop half-length: elevation vs model')
for k in ('C12', 'C13'):
for r in sorted(by[k], key=lambda r: r[0]):
if not r[0].endswith('k2'):
continue
z = r[2][2]
yy = 19515.6 + (z / 1000.0 - 43.411) * 1000
el = [h for yv, h in H.items() if abs(yv - yy) < 1.5]
half = (r[3][0] - r[2][0]) / 2.0
print(k, 'EL %.3f' % (z / 1000), 'model X', r[2][0], r[3][0], 'half %.1f' % half, '| elevation right end %s' % (round(el[0], 1) if el else 'n/a'))
print('--- totals (design length, m) model vs table')
tab = {'C9': 518.820, 'C10': 216.510, 'C11': 536.040, 'C12': 369.234, 'C13': 264.105, 'C14': 147.600, 'C15': 36.192, 'C16': 72.943}
cntm = {'C9': 60, 'C10': 21, 'C11': 60, 'C12': 18, 'C13': 9, 'C14': 41, 'C15': 13, 'C16': 13}
for k in ('C9', 'C10', 'C11', 'C12', 'C13', 'C14', 'C15', 'C16'):
s = sum(r[4] for r in by[k]) / 1000.0
print(k, 'n', len(by[k]), '(table %d)' % cntm[k], 'model %.3f' % s, 'table %.3f' % tab[k], 'diff %.3f' % (s - tab[k]))
@@ -0,0 +1,49 @@
import sys; sys.path.insert(0, "./pylib")
sys.stdout.reconfigure(encoding='utf-8')
import rhino3dm, numpy as np
m = rhino3dm.File3dm.Read('m.3dm')
byidx = {l.Index: l.FullPath for l in m.Layers}
def meshpts(g, n=1):
V=[];F=[];off=0
for fi in range(len(g.Faces)):
k=g.Faces[fi].GetMesh(rhino3dm.MeshType.Any)
if k is None: continue
v=np.array([[p.X,p.Y,p.Z] for p in k.Vertices]); V.append(v)
for i in range(len(k.Faces)):
f=k.Faces[i]; F.append((f[0]+off,f[1]+off,f[2]+off))
if f[2]!=f[3]: F.append((f[0]+off,f[2]+off,f[3]+off))
off+=len(v)
return np.vstack(V), np.array(F)
cop=None; bars=[]
for o in m.Objects:
p=byidx.get(o.Attributes.LayerIndex)
if p=='P1::코핑': cop=o.Geometry
if p=='P1::철근::코핑::C1': bars.append((o.Attributes.Name,o.Geometry))
V,F=meshpts(cop)
print('coping verts',len(V),'faces',len(F))
# sample points on coping triangles
A=V[F[:,0]];B=V[F[:,1]];C=V[F[:,2]]
N=np.cross(B-A,C-A); area=np.linalg.norm(N,axis=1)/2; N=N/np.maximum(np.linalg.norm(N,axis=1)[:,None],1e-12)
rng=np.random.default_rng(1)
pts=[];nrm=[]
for k in range(len(F)):
n=max(1,int(area[k]/400)) # ~20mm grid
r1=rng.random(n);r2=rng.random(n); s=np.sqrt(r1)
P=A[k]*(1-s)[:,None]+B[k]*(s*(1-r2))[:,None]+C[k]*(s*r2)[:,None]
pts.append(P);nrm.append(np.repeat(N[k][None],n,0))
pts=np.vstack(pts);nrm=np.vstack(nrm)
pts=np.vstack([pts,V]);nrm=np.vstack([nrm,np.zeros_like(V)])
print('sample pts',len(pts))
print('name | min clear cover | nearest-face normal | pt z of bar | #verts')
for name,g in bars:
bv,_=meshpts(g)
# subsample
bv=bv[::2]
best=1e9;bi=None
for s in range(0,len(bv),400):
c=bv[s:s+400]
d=np.linalg.norm(c[:,None,:]-pts[None,:,:],axis=2)
j=d.argmin(axis=1); dm=d[np.arange(len(c)),j]
t=dm.argmin()
if dm[t]<best: best=dm[t];bi=(c[t],pts[j[t]],nrm[j[t]])
print(name,'|',round(best,1),'|',np.round(bi[2],2),'| bar',np.round(bi[0],0),'cop',np.round(bi[1],0))
@@ -0,0 +1,27 @@
import sys, collections, ezdxf
sys.stdout.reconfigure(encoding='utf-8')
rows=[]
for l in open('c9_16.txt',encoding='utf-8'):
if l.startswith(' seg') or ' | len ' not in l: continue
p=l.strip().split(' | ')
rows.append((p[0],eval(p[2].split('min ')[1]),eval(p[3].split('max ')[1]),float(p[4].split()[1])))
by=collections.defaultdict(list)
for r in rows: by[r[0].split('-')[2]].append(r)
T={'C9':(60,1407,(2956+1376)/2,750,518.820),'C10':(21,1407,2998,750,216.510),'C11':(60,1407,(2956+1664)/2,750,536.040)}
print('stirrups: model a=Y width, b=Z range+54.3, c=750')
for k,(n,ta,tb,tc,tl) in T.items():
ma=sum(2*(r[2][1]-r[1][1]) for r in by[k]); mb=sum(2*((r[2][2]-r[1][2])+54.3) for r in by[k]); mc=n*2*tc
ta_=n*2*ta; tb_=n*2*tb
print(k,'width part model %.2f table %.2f diff %.2f m | height part model %.2f table %.2f diff %.2f m | total diff %.2f'%(ma/1000,ta_/1000,(ma-ta_)/1000,mb/1000,tb_/1000,(mb-tb_)/1000,(ma+mb+mc-tl*1000)/1000))
T2={'C12':(18,(12228+3151)/2,1407,580,369.234),'C13':(9,(12167+12044)/2,1407,580,264.105)}
print('loops: model a=X range+54.3, b=Y width, c=580')
for k,(n,ta,tb,tc,tl) in T2.items():
ma=sum(2*((r[2][0]-r[1][0])+54.3) for r in by[k]); mb=sum(2*(r[2][1]-r[1][1]) for r in by[k]); mc=n*4*tc
ta_=n*2*ta; tb_=n*2*tb
print(k,'A part model %.2f table %.2f diff %.2f m | B part model %.2f table %.2f diff %.2f m'%(ma/1000,ta_/1000,(ma-ta_)/1000,mb/1000,tb_/1000,(mb-tb_)/1000))
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
print('B-B left/bottom dim texts')
for e in ms:
if e.dxftype()=='TEXT':
p=e.dxf.insert
if 39000<p.x<41500 and 9500<p.y<13800: print((round(p.x),round(p.y)),e.dxf.text)
@@ -0,0 +1,16 @@
import ezdxf, sys
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
def rows(name,x0,x1,y0,y1,cx,yb):
R=defaultdict(list)
for e in ms:
if e.dxftype()=='CIRCLE' and e.dxf.radius==27:
c=e.dxf.center
if x0<c.x<x1 and y0<c.y<y1: R[round(c.y,1)].append(round(c.x-cx))
print('====',name)
for y in sorted(R,reverse=True):
v=sorted(R[y])
if len(v)>3: print(' +%.1f'%(y-yb),'n',len(v),'offsets',v[:6],'...',v[-4:])
rows('정면도 (bars along Y end-on, offsets = X)',12100,17000,8000,10400,12159.4,8115.6)
rows('측면 단면도 (bars along X end-on, offsets = Y)',30100,34100,8000,10400,30133.6,8115.6)
@@ -0,0 +1,27 @@
import ezdxf, sys
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
name=sys.argv[1]; x0,x1,y0,y1=map(float,sys.argv[2:6])
print('=====',name)
conc=defaultdict(list)
cir=defaultdict(list); H=defaultdict(list); V=defaultdict(list)
for e in ms:
t=e.dxftype()
if t=='CIRCLE' and e.dxf.radius==27:
c=e.dxf.center
if x0<c.x<x1 and y0<c.y<y1: cir[round(c.y,1)].append(round(c.x,1))
elif t=='LINE' and e.dxf.layer in('CS-RBAR-MAJR','CS-CONC-MAJR'):
a,b=e.dxf.start,e.dxf.end
if x0<min(a.x,b.x) and max(a.x,b.x)<x1 and y0<min(a.y,b.y) and max(a.y,b.y)<y1:
if abs(a.y-b.y)<0.6: H[(e.dxf.layer[3:7],round(a.y,1))].append((round(min(a.x,b.x),1),round(max(a.x,b.x),1)))
elif abs(a.x-b.x)<0.6: V[(e.dxf.layer[3:7],round(a.x,1))].append((round(min(a.y,b.y),1),round(max(a.y,b.y),1)))
print('circle rows (y: n, x-min..x-max):')
for y in sorted(cir,reverse=True):
xs=sorted(cir[y]); print(' y',y,'n',len(xs),xs[0],'..',xs[-1])
print('H lines:')
for k in sorted(H,reverse=True):
if k[0]=='CONC' or sum(b-a for a,b in H[k])>500: print(' ',k,sorted(H[k])[:2],len(H[k]))
print('V lines (count by x):')
for k in sorted(V):
if k[0]=='CONC' or sum(b-a for a,b in V[k])>300: print(' ',k,sorted(V[k])[:2],len(V[k]))
@@ -0,0 +1,19 @@
import sys,collections
sys.stdout.reconfigure(encoding='utf-8')
by=collections.defaultdict(list); segs={}; cur=None
for l in open('bfoot.txt',encoding='utf-8'):
if l.startswith(' seg'): segs.setdefault(cur,[]).append(l.strip()); continue
if ' | len ' not in l: continue
p=l.strip().split(' | '); nm=p[0]; k=nm.split('-')[2]; cur=k
mn=eval(p[2].split('min ')[1]); mx=eval(p[3].split('max ')[1])
by[k].append(dict(nm=nm,len=float(p[1].split()[1]),mn=mn,mx=mx,dl=float(p[4].split()[1]),dia=p[5].split()[1],shape=p[6].split()[1],src=p[7].split('src ',1)[1] if 'src ' in p[7] else ''))
tot=0
for k in sorted(by,key=lambda s:int(s[1:])):
v=by[k]; f=v[0]
xs=[r['mn'][0] for r in v]; ys=[r['mn'][1] for r in v]; zs=sorted({round(r['mn'][2],1) for r in v}); zm=sorted({round(r['mx'][2],1) for r in v})
print('====',k,'n',len(v),'dia',f['dia'],'shape',f['shape'],'design',sorted({r['dl'] for r in v})[:3],'sum %.3f'%(sum(r['dl'] for r in v)/1000))
print(' example',f['nm'],'| src:',f['src'][:150])
print(' X min..max',round(min(r['mn'][0] for r in v)),round(max(r['mx'][0] for r in v)),'| Y',round(min(r['mn'][1] for r in v)),round(max(r['mx'][1] for r in v)),'| Z',round(min(r['mn'][2] for r in v),1),round(max(r['mx'][2] for r in v),1))
ux=sorted({round(r['mn'][0]) for r in v}); uy=sorted({round(r['mn'][1]) for r in v})
print(' unique X-min',len(ux),ux[:6],'..',ux[-3:],' unique Y-min',len(uy),uy[:6],'..',uy[-3:])
for s in segs.get(k,[])[:8]: print(' ',s)
@@ -0,0 +1,75 @@
import ezdxf, sys, math
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d = ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf')
ms = d.modelspace()
x0, x1, y0, y1 = [float(v) for v in sys.argv[1:5]] # region of the footing view
CX = float(sys.argv[5]) # centre x of the view
ZB = float(sys.argv[6]) # y of footing concrete bottom (drawing)
dots = []; bub = []; lines = []; txt = []; rb = []
for e in ms:
t = e.dxftype()
if t == 'CIRCLE':
c = e.dxf.center
if e.dxf.radius == 27 and x0 < c.x < x1 and y0 < c.y < y1:
dots.append((c.x, c.y))
if e.dxf.radius == 240 and x0 - 3000 < c.x < x1 + 3000 and y0 - 2500 < c.y < y1 + 3500:
bub.append((c.x, c.y))
elif t == 'LINE':
a, b = e.dxf.start, e.dxf.end
if e.dxf.layer in ('CS-DIML', 'CS-SYMB-LIN3', 'CS-SYMB') and x0 - 3500 < min(a.x, b.x) and max(a.x, b.x) < x1 + 3500 and y0 - 2500 < min(a.y, b.y) and max(a.y, b.y) < y1 + 3500:
lines.append(((a.x, a.y), (b.x, b.y)))
if e.dxf.layer == 'CS-RBAR-MAJR' and x0 < min(a.x, b.x) and max(a.x, b.x) < x1 and y0 < min(a.y, b.y) and max(a.y, b.y) < y1:
rb.append(((a.x, a.y), (b.x, b.y)))
elif t == 'TEXT':
p = e.dxf.insert
if x0 - 3500 < p.x < x1 + 3500 and y0 - 2500 < p.y < y1 + 3500:
txt.append((p.x, p.y, e.dxf.text.strip()))
n = len(lines)
par = list(range(n))
def find(i):
while par[i] != i:
par[i] = par[par[i]]; i = par[i]
return i
pts = defaultdict(list)
for i, (a, b) in enumerate(lines):
pts[(round(a[0] / 4), round(a[1] / 4))].append(i); pts[(round(b[0] / 4), round(b[1] / 4))].append(i)
for k, v in pts.items():
for j in v[1:]:
par[find(v[0])] = find(j)
comp = defaultdict(list)
for i in range(n): comp[find(i)].append(i)
def target(p):
# nearest dot within 12, else nearest rebar line within 10 (perpendicular)
best = None
for dd in dots:
dist = math.hypot(p[0] - dd[0], p[1] - dd[1])
if dist < 12 and (best is None or dist < best[0]): best = (dist, 'dot', dd[0], dd[1])
if best: return best
for (a, b) in rb:
if abs(a[1] - b[1]) < 0.6: # horizontal bar line
if min(a[0], b[0]) - 6 <= p[0] <= max(a[0], b[0]) + 6 and abs(p[1] - a[1]) < 8:
return (abs(p[1] - a[1]), 'hline', p[0], a[1])
if abs(a[0] - b[0]) < 0.6: # vertical bar line
if min(a[1], b[1]) - 6 <= p[1] <= max(a[1], b[1]) + 6 and abs(p[0] - a[0]) < 8:
return (abs(p[0] - a[0]), 'vline', a[0], p[1])
return None
for (bx, by) in sorted(bub):
lab = [t for t in txt if abs(t[0] - bx) < 260 and abs(t[1] - by) < 260 and t[2].startswith('B') and t[2][1:].isdigit()]
if not lab: continue
name = lab[0][2]
ul = by - 252
cand = [i for i, (a, b) in enumerate(lines) if abs(a[1] - ul) < 4 and abs(b[1] - ul) < 4 and min(a[0], b[0]) < bx + 300 and max(a[0], b[0]) > bx - 300]
hits = []
seen = set()
for i in cand:
for j in comp[find(i)]:
for p in lines[j]:
tg = target(p)
if tg and (round(tg[2]), round(tg[3]), tg[1]) not in seen:
seen.add((round(tg[2]), round(tg[3]), tg[1]))
hits.append(tg)
out = []
for dist, kind, x, y in hits:
out.append('%s z=%.1f(+%.1f from bottom) xoff=%+.0f' % (kind, 26208 + (y - ZB), y - ZB, x - CX))
print(name, '(bubble %.0f,%.0f)' % (bx, by), '->', out if out else 'no target found (underline lines %d)' % len(cand))
@@ -0,0 +1,118 @@
import sys; sys.path.insert(0,"./pylib")
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
wb=Workbook()
HDR=PatternFill('solid',fgColor='1F3864'); YEL=PatternFill('solid',fgColor='FFF200'); GRN=PatternFill('solid',fgColor='C6EFCE'); GRY=PatternFill('solid',fgColor='E7E6E6')
thin=Side(style='thin',color='999999'); BR=Border(left=thin,right=thin,top=thin,bottom=thin)
FILL={'기준 미적용':YEL,'일치':GRN,'일치(경미 차이)':YEL,'불일치':YEL,'기준 충족':GRN,'기준 충족(여유 적음)':YEL,'미확인':GRY,'해당 없음':GRY,'도면 간 불일치':YEL}
ws=wb.active; ws.title='대조표 C1~C16'
ws['A1']='운곡교 P1 코핑 철근 C1~C16 : 정면도 · 단면도(A-A, B-B) · 라이노 모델 · 재료표 4자 대조표'; ws['A1'].font=Font(bold=True,size=14)
ws['A2']='정면도=D1(C0050205-025) 정면도 / 단면도=D1 단면 A-A / 재료표=D3(C0050205-027) / 모델=운곡교p1.3dm(2026-10-14 저장본). 작성 2026-10-15, 검토 미확정. 판정: 일치 / 일치(경미 차이) / 도면 간 불일치 / 미확인 / 해당 없음(도면에 그 정보가 없음). 노란색 = 보고서에 쓸 차이 항목.'
ws.merge_cells('A2:I2'); ws['A2'].alignment=Alignment(wrap_text=True); ws.row_dimensions[2].height=44
head=['번호','마크','검토 항목','정면도','단면도(A-A)','재료표(D3)','라이노 모델','판정','비고 / 근거']
R=[
['1','C1·C4','철근 높이(중심, 최저 상면 EL 43.411 기준)','100 아래 (y=19415.6, EL 43.311)','치수 100 (점 그림은 104.5)','해당 없음','100 아래 (EL 43.311)','일치(경미 차이)','도면 점 그림만 치수보다 4.5mm 아래. 치수·정면도·모델은 일치'],
['2','C2·C5','철근 높이','200 아래 (y=19315.6, EL 43.211)','치수 200 (점 204.5)','해당 없음','200 아래 (EL 43.211)','일치(경미 차이)','상동 (4.5mm)'],
['3','C3·C6','철근 높이','300 아래 (y=19215.6, EL 43.111)','치수 300 (점 304.5)','해당 없음','300 아래 (EL 43.111)','일치(경미 차이)','상동 (4.5mm)'],
['4','C1','가로(Y) 위치','해당 없음','1단 x=43120.3, 43382.3+250k (Y -1450, -1188 … 1312)','해당 없음','Y -1450, -1188 … 1312 (12본)','일치','단면 지시선 3본(43382, 43632, 43882) 확인 후 250 간격으로 연장'],
['5','C4','가로(Y) 위치','해당 없음','1단 x=43257.3+250k … 46020.3 (Y -1313 … 1187, 1450)','해당 없음','Y -1313 … 1187, 1450 (12본)','일치',''],
['6','C2','가로(Y) 위치','해당 없음','2단 지시선 Y 562, 812, 1062','해당 없음','Y -1450 … 1312 (12본)','일치',''],
['7','C5','가로(Y) 위치','해당 없음','2단 지시선 Y 687, 937, 1187','해당 없음','Y -1313 … 1187, 1450 (12본)','일치',''],
['8','C3','가로(Y) 위치','해당 없음','3단 지시선 Y -1450, -1188, -938','해당 없음','Y -1450 … 1312 (12본)','일치',''],
['9','C6','가로(Y) 위치','해당 없음','3단 지시선 Y -1313, -1063, -813','해당 없음','Y -1313 … 1187, 1450 (12본)','일치',''],
['10','C1~C6','본수 (마크당)','해당 없음','단마다 24점 = 2개 마크 x 12','12','12','일치','단면 점 개수로 추정(24/2). 지시선은 마크당 3본만 가리킴'],
['11','C1~C6','직경 / 형상','H29 @250','H29 @250','H29 / VIII','H29 / VIII','일치',''],
['12','C1·C4','A (수평) / B / C (다리)','치수 없음','치수 없음','A 11.855 / B 1.319 / C 1.315','A 11,855.5 / B 1,319 / C 1,315.6 (곧은 구간+굽힘 접선으로 환산)','일치','도면 3종 중 재료표만 치수 보유. 모델 설계 A=11,855'],
['13','C2·C5','A / B / C','치수 없음','치수 없음','A 11.880 / B 0.101 / C 0.101','A 11,880 / B 101 / C 101 (곧은 구간+접선)','일치',''],
['14','C3·C6','A / B / C','치수 없음','치수 없음','A 11.904 / B 0.101 / C 0.101','A 11,904 / B 101 / C 101','일치',''],
['15','C1·C4','길이 L','치수 없음','치수 없음','14.490 (A+B+C=14.489)','설계 14,489 / 중심선 14,447.1','일치','중심선 -42mm = 모서리 R65 굽힘(재료표는 각진 치수). 재료표 L 14.490 은 14.489 반올림'],
['16','C2·C5','길이 L','치수 없음','치수 없음','12.081 (A+B+C=12.082)','설계 12,082 / 중심선 12,040.1','도면 간 불일치','재료표 L 12.081 이 A+B+C 보다 1mm 작음(표 자체). 중심선 -42mm 는 굽힘 때문'],
['17','C3·C6','길이 L','치수 없음','치수 없음','12.106 (A+B+C=12.106)','설계 12,106 / 중심선 12,064.1','일치','중심선 -42mm 는 굽힘 때문'],
['18','C1~C6','다리 각도 X1 / X2','오른쪽 다리선 기울기 6.43° (= 96.4°). 왼쪽은 미작도','치수 없음','98° / 96°','97.6° / 96.4°','일치(경미 차이)','모델은 다리를 코핑 끝면과 평행하게 작성. 오른쪽은 정면도 작도 기울기(96.4°)와 일치, 재료표 96°는 반올림 값으로 추정. 왼쪽 97.6°는 정면도 없어 미확인'],
['19','C1~C6','피복 (철근 중심, 상면·측면)','100 아래 (정면도 선)','치수 100 / 측면 100 (좌측 100.0)','해당 없음','100 (순피복 약 85.7)','일치','평강1교 120 규칙은 해당 없음(도면 우선)'],
['20','C1~C6','내면반지름 (굽힘)','도면에 없음','도면에 없음','R 칸 빈칸','중심선 R≈65 (내면지름 약 101.5)','기준 충족(여유 적음)','KDS 24 14 21: H29 내면지름 ≥ 3.5db=100.1. 모델 파이프 직경 29 기준으론 거의 한계. 도면은 반지름을 그리지 않음'],
['21','C1~C6','철근 양끝 위치(단부 이격)','미확인','미확인','해당 없음','100.8 / 100.6','미확인','정면도·평면도와 대조 필요'],
['22','C1~C6','단면 점 그림의 세로 위치','해당 없음','치수 대비 +4.5mm 아래','해당 없음','치수와 일치','도면 간 불일치','단면 A-A 하면 1열도 +4.6mm. 중간 열(650~1400)은 정확'],
['23','C7·C8','직경 / 형상','H29 @250 (C7 라벨)','H29 @250','H29 / VII','H29 / VII','일치',''],
['24','C7·C8','본수','해당 없음','하면 1열 24점 = 2개 마크 x 12','12 · 12','12 · 12','일치','점 개수로 추정(24/2)'],
['25','C7','가로(Y) 위치','해당 없음','하면 지시선 Y -1188, -938, -688','해당 없음','Y -1450 … 1312 (12본, 위 3본 포함)','일치','C1과 같은 교차 배치 패턴'],
['26','C8','가로(Y) 위치','해당 없음','하면 지시선 Y 687, 937, 1187','해당 없음','Y -1313 … 1187, 1450 (12본, 위 3본 포함)','일치','C4와 같은 교차 배치 패턴'],
['27','C7·C8','중앙 수평부 길이 (재료표 C)','철근선 x 12159.4~13233.4 = 1,074.0 (반쪽, 전체 2,148)','해당 없음','C = 2.148','2,148 (x ±1,074.0)','일치',''],
['28','C7·C8','중앙 수평부 높이 (EL)','y=16615.6 → EL 40.511 (콘크리트 하면 평면부 EL 40.411 위 100)','해당 없음','해당 없음','EL 40.511','일치',''],
['29','C7·C8','곡선 반지름 (재료표 R·r1·r2·B·D·J)','철근 호 R 12,100, 콘크리트 하면 호 R 12,000과 중심 동일 (중심 19111.2, 6039.2)','해당 없음','R·B·D·J 칸 빈칸','R 12,100 (하면과 동심)','일치','재료표 빈칸은 도면 누락(선행보고서 M-04). 정면도에서 R 12,100 확인되어 모델 해석 근거 확보'],
['30','C7·C8','호 길이','R 12,100 x 24.973° = 5,273.9','해당 없음','직접 표기 없음 (L에서 역산 5,273.0)','5,273.0','일치(경미 차이)','0.9mm'],
['31','C7·C8','호 바깥 끝 위치','x ±6,088.9, EL 42.004','해당 없음','해당 없음','x ±6,087.9, EL 42.004','일치(경미 차이)','1mm'],
['32','C7·C8','다리 길이 (재료표 A·E)','C1 다리선(1,315.5)과 겹쳐 650 구간 표기 없음','해당 없음','A 650 / E 650','650 / 650','일치','정면도에서 650은 직접 확인 못 함(재료표↔모델만 일치)'],
['33','C7·C8','다리 기울기 (끝면과 평행)','오른쪽 다리선 6.43° (x 18248.3→18101.0, y 18108.4→19415.6)','해당 없음','X1·X2 칸 빈칸','6.4°','일치','왼쪽은 정면도에 안 그려져 미확인'],
['34','C7·C8','길이 L','철근선 환산 약 13,995.8','해당 없음','13.994','설계 13,993.99 / 중심선 13,993.99','일치','굽힘 반지름을 넣지 않은 각진 모델이라 중심선=재료표'],
['35','C7·C8','피복 (하면, 철근 중심)','100 (평면부 100, 호는 하면 R12,000에 동심 +100)','치수 110 (점 그림은 하면에서 105.0)','해당 없음','100 (하면 순거리 85.5~86.0)','도면 간 불일치','단면 A-A 하면 110 ↔ 정면도 100: 약 9.5mm. 단면 절단 위치(좌측 끝에서 약 400mm)에서 수직 환산 100.5. 상면은 100으로 일치했는데 하면만 다름'],
['36','C7·C8','내면반지름 (굽힘 4곳: 호-다리 약 79° x2, 평면부-호 약 29° x2)','도면에 없음','도면에 없음','R 칸 빈칸','4곳 모두 각진 연결(굽힘 반지름 없음)','기준 미적용','KDS 24 14 21 내면지름 ≥ 3.5db 미반영. C1~C6은 필렛 적용됨. 적용하면 중심선 약 37mm 짧아짐(재료표 L 대비 -0.3%)'],
# ---- 단면 B-B
['37','C1~C6','[단면 B-B] 철근 높이','100/200/300 아래 (EL 43.311/.211/.111)','B-B 점: 최저상면 기준 104.5/204.5/304.5 아래 (B-B 상면 EL 43.555 기준 248.5/348.5/448.5)','해당 없음','100/200/300 아래','일치(경미 차이)','A-A와 같은 +4.5mm. B-B 상면은 정면도 3번째 계단(EL 43.555)'],
['38','C1~C6','[단면 B-B] 가로(Y) 위치','해당 없음','지시선 C1 -1188/-938/-688, C4 -1063/-813/-563, C2 562/812/1062, C5 687/937/1187, C3 -1450/-1188/-938, C6 -1313/-1063/-813','해당 없음','모델 Y 배치와 일치 (A-A와 동일)','일치',''],
['39','C7·C8','[단면 B-B] 하면 높이 / 하면 치수','평면부 EL 40.511 (하면 EL 40.411 위 100)','점 EL 40.5155 (하면 위 104.5), B-B 하면 치수 100','해당 없음','EL 40.511 (100)','일치(경미 차이)','점 +4.5mm. B-B 하면 치수는 100으로 정면도·모델과 일치 → A-A 하면 110 만 다름(행 35)'],
['40','C7·C8','[단면 B-B] 가로(Y) 위치','해당 없음','지시선 C7 -1188/-938/-688, C8 687/937/1187','해당 없음','모델과 일치','일치',''],
['41','C16','[단면 B-B] 점 13개 (단차 줄)','선 EL 43.455 (x 0~4066, 오른쪽 반만)','점 EL 43.4505 (B-B 상면 -104.5), Y -1450,-1313,-1063 … 1187,1450 (13개)','13본, A 5.261 B 350 L 5.611','EL 43.4547, 13본, Y 동일','일치(경미 차이)','점 4.2~4.5mm 어긋남(같은 유형)'],
['42','C12·C13','[단면 B-B] 폐합근 단 위치','가로선 EL 42.761(C13)·42.511·42.261 / 42.011(C12)·41.761·…·40.761','9행 EL 42.761~40.761 (250 간격), 행당 6점 Y ±317/±843/±1423','C12 18본 / C13 9본','C13 3단 + C12 6단, 각 3루프, Y ±317/±843/±1423','일치','본수: C13 = 3단x3루프 = 9, C12 = 6단x3루프 = 18. A-A 지시선으로 C13=위 3행, C12=아래 행 확인'],
['43','C12·C13','길이 방향 A (정면도 우측 끝 / 재료표 / 모델)','우측 끝 반길이: C13 6003.6/6031.7/6059.9, C12 6088.1/4393.4/3431/2695/2081/1548.3','해당 없음','C12 A 12.228~3.151, C13 A 12.167~12.044','모델 반길이 일치(오차 ≤0.5mm). 모델 A 중심선+54.3 = 12,228.2~3,150.9 / 12,044.3~12,166.8','일치','재료표 A 는 도면 중심선 길이 + 54.3 규칙과 일치(선행보고서 7.2)'],
['44','C12·C13','폭 B','해당 없음','루프 폭 1106 / 1686 / 1106 (A-A, B-B 점 Y ±317, ±843, ±1423)','B = 1.407 (3루프 동일)','1106 / 1686 / 1106','도면 간 불일치','선행보고서 E-08 확인. 재료표 vs 도면(단면)=모델'],
['45','C9·C10·C11','스터럽 개수·위치 (정면도 수직선)','우측 24위치: x=0,300,600,900 / 1150~5900 @250','단면 3루프 (A-A, B-B)','C9 60 / C10 21 / C11 60','C9 20위치(-5900~-1150 @250) / C10 7위치(-900~900 @300) / C11 20위치(1150~5900) x 3루프','일치','좌측(C9 구간)은 정면도에 안 그려져 모델의 대칭 배치는 가정 → C9 위치 미확인'],
['46','C10·C11','스터럽 상·하단 높이 (정면도)','하단 EL 40.511→41.989 (C7·C8 호 +100), 상단 EL 43.455 / 43.598','해당 없음','해당 없음','24개 위치에서 최대 0.07mm 차이','일치',''],
['47','C9','스터럽 높이 (좌측)','좌측 미작도','해당 없음','B 1.376~2.956','모델 상단 EL 43.311~43.455, 하단 C7·C8 호 +100','미확인','정면도 우측과 대칭 가정'],
['48','C9~C11','높이 B','중심선 높이 (정면도)','단면은 +45mm (행 50)','C9 2.956~1.376 / C10 2.998 / C11 2.956~1.664','모델 b = 정면도 중심선 높이 + 54.3 (C9 1,376.5 / C10 2,998.3 / C11 1,663.4~2,956)','일치','재료표 B 도 같은 규칙. 단 재료표 L 은 B 를 (최대+최소)/2 로 평균 (행 52)'],
['49','C9~C11','폭 A','해당 없음','1160 / 1740 / 1160 (A-A, B-B)','A = 1.407 (3루프 동일)','1160 / 1740 / 1160','도면 간 불일치','선행보고서 E-07 확인. 재료표 vs 도면(단면)=모델'],
['50','C9~C11','스터럽 높이: 단면 vs 정면도','정면도·모델: 상·하 100 안쪽 (C10 높이 2,944)','A-A·B-B: 상 77.5, 하 77.5(A-A 87.1) 안쪽 (B-B 높이 2,989)','해당 없음','100 안쪽 (정면도 따름)','도면 간 불일치','단면이 정면도보다 약 45mm 큼 (상·하 각 22.5). 선행보고서에 없던 항목'],
['51','C9~C11','겹침 C','도면 미작도','도면 미작도','C = 750','길이에만 반영 (2x750)','일치','도면에 겹침 표현 없음'],
['52','C9~C13','총길이 (모델 설계길이 / 재료표)','해당 없음','해당 없음','C9 518.820 / C10 216.510 / C11 536.040 / C12 369.234 / C13 264.105','C9 488.519 / C10 214.261 / C11 508.339 / C12 333.318 / C13 262.168','불일치','차이 원인 분해(m): C9 -30.30 (폭 -6.44, 높이평균근사 -23.80) / C10 -2.25 (폭) / C11 -27.70 (폭 -6.44, 높이평균근사 -21.26) / C12 -35.92 (A평균근사 -32.04, 폭 -3.88) / C13 -1.94 (폭). 재료표 L 의 (최대+최소)/2 근사가 폭보다 큰 원인'],
['53','C14','본수·형상·길이','우측 23점 (대칭 가정, +2913 상단 1점은 우측만 → 41)','A-A 가로선 EL 43.189/43.089/42.511/42.011','41본 H19, A 2.900 B 350 L 3.600, 총 147.600','41본, 총 147.600','일치','본수 41 은 우측 점 23 + 좌측 대칭 18 로 맞춤(추정)'],
['54','C14','위치·높이 (정면도 점 vs 모델)','x=13/913/1913/2913/3913/4913 @1000, 상단 두 줄 EL 43.188/43.088 (모든 위치)','해당 없음','해당 없음','상단 두 줄·x=13/913/3913/4913 일치. x=1913 최하단: 정면도 41.011 / 모델 41.361, x=2913 최하단: 정면도 41.511 / 모델 41.861','불일치','모델이 최하단 점 2개를 350mm 올림(콘크리트 하면 EL 40.826/41.235 와 훅 간섭 때문으로 추정, 근거 기록 없음). 확인 필요'],
['55','C14','훅 방향','도면에 없음','도면에 없음','III (양끝 갈고리) B 350','아래로 가정 [추론]','미확인','훅이 위/아래 어느 쪽인지 도면에 없음. x=+2913 상단 점(EL 43.442)은 위로 불가'],
['56','C15·C16','형상·위치·길이','C15 EL 43.598, x 3816~5900 양끝 350 아래 / C16 EL 43.455, x 0~4066','B-B 에 C16 점 13개 (행 41)','C15 13본 L 2.784, C16 13본 L 5.611','동일 위치·높이, 총길이 36.192 / 72.943 정확히 일치','일치','C16 왼쪽 끝(x -1195)과 훅은 정면도 미작도 → 모델 추정'],
['57','불명','단면 B-B 상면 -100 가로선 (EL 43.455, 길이 2,900)','해당 없음','B-B 상면에서 100 아래 전폭(Y ±1450) 선 1개','해당 없음','C14 의 EL 43.455 점은 x=2913 에서 EL 43.442 1개뿐','미확인','이 선이 어느 마크인지 불명 (C14 상단 점으로 추정). B-B 절단 위치 정보 필요'],
]
for j,h in enumerate(head,1):
c=ws.cell(4,j,h); c.font=Font(bold=True,color='FFFFFF'); c.fill=HDR; c.alignment=Alignment(horizontal='center',vertical='center',wrap_text=True); c.border=BR
for i,row in enumerate(R,5):
for j,v in enumerate(row,1):
c=ws.cell(i,j,v); c.border=BR; c.alignment=Alignment(vertical='center',wrap_text=True,horizontal='center' if j in(1,2,8) else 'left')
ws.cell(i,8).fill=FILL.get(row[7],GRY); ws.cell(i,8).font=Font(bold=True)
ws.row_dimensions[i].height=48
for j,w in enumerate([6,9,26,26,30,26,34,16,48],1): ws.column_dimensions[get_column_letter(j)].width=w
ws.freeze_panes='D5'
w2=wb.create_sheet('차이 항목 요약')
d=[['번호','구분','내용','어느 도면끼리','크기','처리'],
['D-1','도면 내부','단면 A-A 철근 점 그림이 치수 기준보다 아래로 그려짐 (상면 3단 +4.5mm, 하면 1열 +4.6mm)','단면 A-A 점 ↔ 단면 치수 · 정면도 · 모델','4.5mm','치수 우선, 모델 수정 불필요. 도면 정정 요청 사항 (보고서 E-06과 같은 유형)'],
['D-2','도면 내부','재료표 C2·C5 L 12.081 이 A+B+C = 12.082 와 다름','재료표 내부','1mm','반올림 오차로 추정, 확인 요청'],
['D-3','모델-도면','다리 각도 재료표 98°/96° ↔ 모델 97.6°/96.4°','재료표 ↔ 모델','0.4°','모델은 정면도 작도 기울기를 따름(오른쪽 6.43°=96.4° 확인). 재료표 값은 반올림으로 추정, 왼쪽은 정면도 없어 미확인'],
['D-4','모델-도면','모델 중심선 길이가 재료표 L보다 짧음 (C1·C4 -42mm, C2·C3·C5·C6 -41~42mm)','재료표 ↔ 모델','약 42mm','정상: 재료표는 각진 치수, 모델은 R65 굽힘 반영'],
['D-5','기준','내면반지름이 KDS 최소값에 근접 (모델 파이프 직경 29 기준)','모델 ↔ KDS 24 14 21','여유 약 0.4mm','반지름 확대 여부 결정 필요'],
['D-6','미확인','철근 양끝(단부 이격 100.8) 도면 대조','정면도·평면도 ↔ 모델','-','다음 검토'],
['D-7','도면 내부','C7·C8 하면 피복: 단면 A-A 치수 110 ↔ 단면 B-B 치수 100 · 정면도(하면에서 100, R12,100/R12,000 동심) · 모델 100','단면 A-A ↔ B-B · 정면도 · 모델','약 9.5mm','A-A 하면 치수만 다름(오기 가능성). 모델은 100 유지, 도면 확인 요청'],
['D-8','모델-기준','C7·C8 굽힘 4곳에 내면반지름(KDS 3.5db) 미반영, 각진 연결','모델 ↔ KDS 24 14 21','중심선 약 37mm','모델에 굽힘 반지름 적용 여부 결정 (C1~C6은 적용됨)'],
['D-9','도면 내부','재료표 C7·C8 형상 VII 의 B·D·R·J(r1·r2·j) 칸이 빈칸. 정면도에는 R 12,100 호가 작도되어 있음','재료표 ↔ 정면도','-','재료표 보완 요청 (선행보고서 M-04). 모델은 정면도 R 12,100 사용'],
['D-10','미확인','C7·C8 다리 650 은 정면도에서 C1 다리선과 겹쳐 직접 확인 불가, 왼쪽 다리도 정면도에 없음','정면도 ↔ 재료표·모델','-','재료표 650과 모델 650은 일치. 정면도로는 미확인'],
['D-11','도면 내부','스터럽 C9·C10·C11 폭: 재료표 A 1.407 ↔ 단면 1.160/1.740/1.160 (모델은 단면 따름)','재료표 ↔ 단면 A-A·B-B','3루프 합 4,221 ↔ 4,060','선행보고서 E-07 확인. 재료표 정정 또는 도면 정정 필요'],
['D-12','도면 내부','폐합근 C12·C13 폭: 재료표 B 1.407 ↔ 단면 점 간격 1106/1686/1106 (+54.3 = 1160/1740) (모델은 단면 따름)','재료표 ↔ 단면 A-A·B-B','C12 -3.88 m, C13 -1.94 m','선행보고서 E-08 확인'],
['D-13','재료표 근사','C9·C11·C12 재료표 L 이 변하는 치수를 (최대+최소)/2 로 평균 → 모델 정확 길이와 큰 차이','재료표 ↔ 모델','C9 -23.80 m, C11 -21.26 m, C12 -32.04 m','선행보고서는 폭 차이를 원인으로 봤으나 실제로는 평균 근사가 더 큼. 본별 길이 목록 요청'],
['D-14','도면 내부','스터럽 높이: 단면 A-A·B-B 가 정면도·모델보다 약 45mm 큼 (상·하 각 22.5, 단면 77.5 vs 정면도 100 안쪽)','단면 ↔ 정면도 · 모델','약 45mm','선행보고서에 없던 항목. 확인 필요'],
['D-15','모델-도면','C14 최하단 점 2개: x=1913 정면도 41.011 / 모델 41.361, x=2913 정면도 41.511 / 모델 41.861','정면도 ↔ 모델','350mm','모델이 올린 근거 기록 없음(하면·훅 간섭 추정). 확인 필요'],
['D-16','미확인','C14 훅 방향(아래 가정), C9 좌측 스터럽(정면도 미작도), C16 왼쪽 끝, 단면 B-B 상면 -100 가로선 정체','-','-','도면 보완 또 판단 필요']]
for i,row in enumerate(d,1):
for j,v in enumerate(row,1):
c=w2.cell(i,j,v); c.border=BR; c.alignment=Alignment(wrap_text=True,vertical='center')
if i==1: c.font=Font(bold=True,color='FFFFFF'); c.fill=HDR
for j,w in enumerate([8,12,58,30,12,52],1): w2.column_dimensions[get_column_letter(j)].width=w
for i in range(2,18): w2.row_dimensions[i].height=44
w3=wb.create_sheet('미검토 마크')
rows=[['마크','형상','직경','재료표 본수','상태']]
for m,sh,dia,n in [('S1','III','H19','50'),('S2','III','H19','105'),('B1~B12','-','-','-'),('P1~P6','-','-','-')]:
rows.append([m,sh,dia,n,'미검토'])
for i,row in enumerate(rows,1):
for j,v in enumerate(row,1):
c=w3.cell(i,j,v); c.border=BR; c.alignment=Alignment(horizontal='center')
if i==1: c.font=Font(bold=True,color='FFFFFF'); c.fill=HDR
elif j==5: c.fill=GRY
for j,w in enumerate([14,8,8,12,10],1): w3.column_dimensions[get_column_letter(j)].width=w
out=r'D:\★Pier 자료\심수석님 검토 파일\검토_운곡교P1\대조표_정면도_단면도_모델_재료표_C1-C16.xlsx'
wb.save(out); print('saved')
@@ -0,0 +1,692 @@
# -*- coding: utf-8 -*-
"""운곡교 P1 철근재료표 대조 (도면 재료표와 같은 양식으로 도면표 재현 → 모델표 작성 → 대조·비고).
사용: python -I make_bom_compare.py <pylib경로> <출력xlsx>
입력(고정): 바이브코딩 폴더의 3.dxf (도면 재료표), 운곡교p1.3dm (모델)
시트: ①도면 재료표(재현) ②모델 재료표 ③대조·비고 ④요약 ⑤비고코드설명 ⑥모델_개별철근
"""
import sys, re, math, collections
sys.path.insert(0, sys.argv[1])
import ezdxf, rhino3dm
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
BASE = r"D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩"
DXF3 = BASE + r"\3.dxf"
M3DM = BASE + r"\운곡교p1.3dm"
OUT = sys.argv[2]
UNIT = {"32": 6.230, "29": 5.040, "25": 3.980, "22": 3.040, "19": 2.250, "16": 1.560}
TOL = 1.0 + 1e-6 # mm : 도면 값은 mm 로 반올림 표기
# =============================================================== 1. 도면 재료표 파싱
COLS = [("num", 1900, 2500), ("shape", 2500, 3500), ("dia", 3500, 4400), ("n", 4400, 5300),
("A", 5300, 6700), ("B", 6700, 8200), ("C", 8200, 9700), ("D", 9700, 11200),
("E", 11200, 12700), ("R", 12700, 14200), ("X1", 14200, 15600), ("X2", 15600, 17100),
("J", 17100, 18800), ("L", 18800, 20500), ("tot", 20500, 21400), ("unit", 21400, 22500),
("w", 22500, 23600), ("wh", 23600, 25000)]
OFF = 23750
doc = ezdxf.readfile(DXF3)
texts = []
for e in doc.modelspace():
if e.dxftype() == "TEXT":
texts.append((e.dxf.insert.x, e.dxf.insert.y, e.dxf.text))
elif e.dxftype() == "MTEXT":
texts.append((e.dxf.insert.x, e.dxf.insert.y, e.plain_text()))
rowsraw = collections.defaultdict(lambda: collections.defaultdict(list))
for x, y, t in texts:
if not (18800 < y < 30000):
continue
tab = "SD500" if x >= 25000 else "SD400"
xx = x - OFF if tab == "SD500" else x
if 8500 <= xx < 12500 and "계" in t:
rowsraw[(tab, round(y / 10) * 10)]["label"].append(t.strip().replace("%%%", "%"))
continue
for name, lo, hi in COLS:
if lo <= xx < hi:
rowsraw[(tab, round(y / 10) * 10)][name].append((xx, t.strip().replace("%%%", "%")))
break
def first(c, k):
v = c.get(k)
if not v:
return ""
return sorted(v)[0][1] if isinstance(v[0], tuple) else v[0]
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:
if y > 25000 + 100 and tab == "SD500" and "겹이음" in lab:
pass
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}
drow[tab].append({"kind": "bar", "mark": num, "cells": d})
# 겹이음 총계 행(라벨 위치가 달라 별도 수집): 도면 값만
for (tab, y), c in rowsraw.items():
pass
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
# 도면 행의 ditto(") 해소
for tab in drow:
last = {}
for r in drow[tab]:
if r["kind"] != "bar":
continue
for k in ("shape", "dia"):
if r["cells"][k] == '"':
r["cells"][k + "_v"] = last[k]
else:
r["cells"][k + "_v"] = r["cells"][k]
last[k] = r["cells"][k]
r["dia_n"] = re.sub(r"\D", "", r["cells"]["dia_v"])
DR = {r["mark"]: r for tab in drow for r in drow[tab] if r["kind"] == "bar"}
# =============================================================== 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 clen(c, n=3000):
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 corner_seq(g):
"""직선 구간을 연장해 만든 꼭짓점 사이 거리(각진 치수). 닫힌 곡선은 시작/끝 변을 합침."""
lines = [(V(s.PointAtStart), V(s.PointAtEnd)) for s in segs_of(g) if is_line(s)]
closed = nrm(sub(V(g.PointAtStart), V(g.PointAtEnd))) < 1e-3
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)))
if closed:
out = [nrm(sub(pts[i + 1], pts[i])) for i in range(len(pts) - 1)]
out.append(nrm(sub(pts[0], lines[0][0])) + nrm(sub(lines[-1][1], pts[-1])))
return out, lines, pts
seq = [lines[0][0]] + pts + [lines[-1][1]]
return [nrm(sub(seq[i + 1], seq[i])) for i in range(len(seq) - 1)], lines, pts
def ang_at(corner, a, b):
va = sub(a, corner); vb = sub(b, corner)
c = dot(va, vb) / (nrm(va) * nrm(vb))
return math.degrees(math.acos(max(-1, min(1, c))))
def num(s):
try: return float(s)
except Exception: return 0.0
def bar_dims(shape, us, g):
"""한 철근의 도면 재료표 항목(A,B,C,D,E,R,X1,X2) 을 mm / 도(°) 로 반환"""
d = {}
if shape in ("I", "XVI"):
d["A"] = corner_seq(g)[0][0]
elif shape in ("III", "XII"):
seq = corner_seq(g)[0]
d["A"] = seq[1]; d["B"] = max(seq[0], seq[2]) if abs(seq[0] - seq[2]) > 1 else seq[0]
if abs(seq[0] - seq[2]) > 1: d["B2"] = min(seq[0], seq[2])
elif shape == "II":
seq = corner_seq(g)[0]
d["A"] = max(seq); d["B"] = min(seq)
elif shape == "VIII":
seq, lines, pts = corner_seq(g)
d["A"] = seq[1]; d["B"] = seq[0]; d["C"] = seq[2]
d["X1"] = ang_at(pts[0], lines[0][0], pts[1]); d["X2"] = ang_at(pts[1], lines[2][1], pts[0])
elif shape == "VII":
ls = [s for s in segs_of(g) if is_line(s)]
arcs = [s for s in segs_of(g) if type(s).__name__ == "ArcCurve"]
d["A"] = nrm(sub(V(ls[0].PointAtEnd), V(ls[0].PointAtStart)))
d["C"] = nrm(sub(V(ls[1].PointAtEnd), V(ls[1].PointAtStart)))
d["E"] = nrm(sub(V(ls[2].PointAtEnd), V(ls[2].PointAtStart)))
d["R"] = arcs[0].Arc.Radius
elif shape == "IV":
d["A"] = num(us.get("w_model")); d["B"] = num(us.get("b_sched"))
mm_ = re.search(r"laps?\s*\d+\s*x\s*(\d+)", us.get("src", ""))
d["C"] = float(mm_.group(1)) if mm_ else None
elif shape == "X":
d["A"] = num(us.get("a_sched")); d["B"] = num(us.get("w_model"))
mm_ = re.search(r"laps?\s*\d+\s*x\s*(\d+)", us.get("src", ""))
d["C"] = float(mm_.group(1)) if mm_ else None
elif shape == "XIII":
seq = corner_seq(g)[0]
d["A"] = seq[0]; d["B"] = seq[1]
elif shape == "XIV":
ls = [s for s in segs_of(g) if is_line(s)]
arcs = [s for s in segs_of(g) if type(s).__name__ == "ArcCurve"]
d["A"] = max(nrm(sub(V(s.PointAtEnd), V(s.PointAtStart))) for s in ls)
d["C"] = min(nrm(sub(V(s.PointAtEnd), V(s.PointAtStart))) for s in ls)
d["B"] = arc_len(arcs[0]); d["R"] = arcs[0].Arc.Radius
elif shape in ("V", "IX"):
s = segs_of(g)[0]
d["A"] = arc_len(s); d["B"] = num(us.get("coupler_length")); d["R"] = s.Arc.Radius
elif shape == "VI":
segs = segs_of(g)
main = max((s for s in segs if type(s).__name__ == "ArcCurve"), key=lambda s: s.Arc.Radius)
ls = [s for s in segs if is_line(s)]
d["R"] = main.Arc.Radius
# A·B 는 갈고리(약 142° 굽힘) 각진 치수 정의가 도면에 없어 기하로 확정할 수 없음 → 미추출(None)
# (접선 연장 방식으로 재면 A 2.837 / B 212 로 도면 2.818 / 200 과 다르고 합 A+2B 도 모델 L 과 안 맞음)
elif shape == "XI":
seq = corner_seq(g)[0]
d["A"] = seq[1]; d["B"] = seq[0]
d["C"] = num(us.get("design_length")) - seq[0] - seq[1] # 상단 180° 연장(미작도, 길이만 반영)
return d
bars = collections.defaultdict(dict)
for o in m3.Objects:
L = lay.get(o.Attributes.LayerIndex, "")
if not L.startswith("P1::철근"):
continue
g = o.Geometry; nm = o.Attributes.Name or ""
if isinstance(g, rhino3dm.Brep):
us = dict(o.Attributes.GetUserStrings())
bars[us.get("mark")].setdefault(us.get("occurrence"), {}).update(us=us, name=nm, layer=L)
elif L.endswith("중심선"):
p = nm.split("-")
if len(p) >= 4 and p[1] == "RC":
b = bars[p[2]].setdefault("-".join(p[3:]), {})
b["g"] = g
mod = {}
for mk, dd in bars.items():
if mk is None:
continue
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"])
for k, v in bar_dims(shape, us, b["g"]).items():
if v is not None:
dims[k].append(v)
half = mk in ("P3", "P4")
n = len(items) // 2 if half else len(items)
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"):
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"):
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]]
if key == "A" or key == "B" or key == "C":
# 도면은 '큰값~작은값'
return fmt_one(hi, key) + "~" + fmt_one(lo, key), [hi, lo]
return fmt_one(hi, key) + "~" + fmt_one(lo, key), [hi, lo]
def same(dtxt, mvals, key):
"""도면 표기(dtxt) 와 모델 값 일치 여부. 반환: 'same' | 'diff' | 'drawing_blank' | 'model_blank' | 'both_blank'"""
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"):
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", "J"]
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", "C4"):
NOTE[mk] = "[D-8] 도면 내부: 재료표 다리각 98°/96° ↔ 정면도 96.4°."
for mk in ("C2", "C5"):
NOTE[mk] = "[D-7] 도면 내부: 재료표 L 12.081 ↔ 치수 합 A+B+C 12.082 (모델은 합계 12.082). [D-8] 다리각 98°/96° ↔ 정면도 96.4°."
for mk in ("C3", "C6"):
NOTE[mk] = "[D-8] 도면 내부: 재료표 다리각 98°/96° ↔ 정면도 96.4°."
for mk in ("C7", "C8"):
NOTE[mk] = "[D-9] 도면 내부: 재료표 B·D·R·X·J 빈칸 → 모델은 정면도 R 12,100(하면 R 12,000 동심)·평면부 EL 40.511 로 작성. [D-10] 다리 650 정면도 겹침으로 확인 불가."
NOTE["C9"] = "[D-4] 스터럽 폭 도면 재료표 1.407 ↔ 단면 작도 1.160/1.740/1.160 (모델은 작도값). [D-6] 재료표 L=(최대+최소)/2 근사 → 모델은 철근마다 개별 변길이. [M-5] 길이 산식 +54.3 규칙은 변하는 치수에만 적용."
NOTE["C10"] = "[D-4] 스터럽 폭 1.407 ↔ 작도 1.160/1.740 (모델은 작도값). [M-5] +54.3 규칙."
NOTE["C11"] = "[D-4] 스터럽 폭 1.407 ↔ 작도 1.160/1.740/1.160. [D-6] (최대+최소)/2 근사 vs 모델 개별 변길이. [M-5] +54.3 규칙."
NOTE["C12"] = "[D-5] 수평폐합근 폭 1.407 ↔ 작도 1.106/1.686/1.106 (모델은 작도값). [D-6] 길이 (최대+최소)/2 근사 vs 모델 개별. [M-5] +54.3 규칙."
NOTE["C13"] = "[D-5] 수평폐합근 폭 1.407 ↔ 작도 1.106/1.686/1.106. [D-6] (최대+최소)/2 근사 vs 모델 개별. [M-5] +54.3 규칙."
NOTE["C14"] = "[M-1] 모델 최하단 점 2개(x=±1913, ±2913)가 도면보다 350 mm 높음(근거 기록 없음). [M-2] 훅 방향 도면 표기 없음 — 모델은 아래로 가정, 도면 점 위치로는 하부 점은 위·상부 점은 아래일 가능성(추정, 간접 확인)."
NOTE["C16"] = "훅 방향·정면도 좌측 끝은 도면 직접 표기 없음 (재료표 L 5.611 = 5.261 + 0.350 한쪽 훅으로 간접 확인)."
for mk in ("S1", "S2"):
NOTE[mk] = "[M-6] 높이 기준: 도면(D2 전열 상세)은 대좌 상면 -50 ↔ 모델은 코핑 상면 -50 (139~164 mm 낮음). [M-7] 층 순서: 도면은 S2 점이 S1 선 위 ↔ 모델은 S1 위·S2 50 mm 아래(모델 src '추론')."
NOTE["UA1"] = "[M-13] 모델에 없음(0본). 도면은 기초 조립용 '배치간격 예시도'(1.0×1.2 예시)라 수량 73본 근거 미확인."
NOTE["P1"] = "[D-20] 주철근 상단: 정면도 EL 43.011 ↔ 재료표 EL 43.061 (모델은 재료표). [D-21] 반지름 치수·선 875 ↔ 점 870.5 (모델은 점)."
NOTE["P2"] = "[D-20] 주철근 상단: 정면도 EL 40.411 ↔ 재료표 EL 43.061 (코핑 정착 2.65 m 차이, 모델은 재료표). [D-21] 반지름 830 ↔ 점 825.5 (모델은 점)."
NOTE["P3"] = "모델은 반원 2조각(72개)=1링 → 개수는 링 수(36)로 환산. A·B 는 모델 갈고리(굽힘 142°·R42.75)의 각진 치수 정의가 도면에 없어 기하로 추출하지 못함(미추출) — 길이 L 6.436·R 897·개수는 일치. [M-12] 단마다 90° 엇갈린 이음 각도는 도면에 기준 없음(모델 가정, 간접)."
NOTE["P4"] = "모델은 반원 2조각(18개)=1링(커플러 65 포함 L 5.636) → 개수 9 로 환산. [D-23] 라벨 H19 @100 ↔ 치수 8@120=960 (모델 @120 — 구조계산서 확인 사항, E-01). [M-12] 커플러 각도 도면에 없음."
NOTE["P5"] = "[D-23] 라벨 @100 ↔ 치수 22@120=2,640 (모델 @120, 구조계산서 확인). [D-22] 링 점 902 ↔ R 897. [M-12] 커플러 각도 도면에 없음."
NOTE["P6"] = "정면도 선 반길이 875 ↔ 모델 직선 894 (한쪽 19 mm, 경미)."
NOTE["B1"] = "[M-8] 모델 교차 철근 간격 54~56 mm ↔ 도면 두 뷰 모두 27~28 mm(점 값을 서로 다른 뷰에서 가져옴: B1 2073 ↔ B5 2127). [D-15] 같은 철근을 점/선으로 그린 높이가 정면도·측면도에서 27~28 mm 다름."
NOTE["B2"] = "[M-9] 다리 끝 +2072 (도면 선 기준 +2100). [D-15] 점/선 높이 27~28 mm 차."
NOTE["B3"] = "[D-17] 도면 중앙 치수 33@250+450(36본) ↔ 점·재료표 38본 (모델 38)."
NOTE["B4"] = "[M-8] 하면 교차 철근 간격 영향(B2·B4)."
NOTE["B5"] = "[M-9] 모델 중심 73 아래(순피복 58.5 mm, 도면 표기 상면 100, 선행 E-06). [M-8] 교차 철근 간격."
NOTE["B6"] = "[M-9] 다리 끝 +2128 (도면 선 기준 +2100, 선행 E-06 피복 57.5). [M-8] 교차 철근 간격."
NOTE["B7"] = "[M-8] 교차 철근 간격."
NOTE["B9"] = "[M-01 선행] 도면 개수 빈칸. 라벨 H16 @500 + 격자 34×29(@250) 체크무늬 986/2 = 493본을 총길이 1,062.415 ÷ L 2.155 로 역산(간접 확인) → 모델 493본. 상단 180° 연장 C=64 는 모델에 직선으로 작도되지 않고 길이만 반영."
NOTE["B10"] = "[D-18] 치수 9@200(10단) ↔ 점·재료표 9단 (모델 9단), 표 9.198×7.644 ↔ 점 ±4593/±3818."
NOTE["B11"] = "B11·B12 4단 높이(z 550)와 위치 일치."
NOTE["B12"] = "[D-16] 수량 34본(재료표) ↔ 모델 28본. 도면이 간소화되어 작도에 일부만 남아 있어 모델 작성 시 도면을 정확히 인식하지 못함. 정면도 간격 1000 한 줄, 단면도 400 위치에서 엇갈려 1000 간격으로 배치되는 철근이라 재료표 34본이 맞는 것으로 보임 → 설계자의 판단(확인)이 필요한 부분."
for mk in ("C1", "C2", "C3", "C4", "C5", "C6"):
KDS[mk] = "[M-4] 내면반지름 여유 거의 없음: 모델 내면지름 101.5 mm ↔ 3.5db 100.1/101.5 mm (H29, 표 4.5-1). 도면에 내면반지름 표기 없음."
for mk in ("C7", "C8"):
KDS[mk] = "[M-3] 굽힘 4곳이 각진 연결(내면반지름 미반영) → 내면지름 3.5db(H29 100.1 mm) 미적용 (KDS 24 14 21 표 4.5-1)."
KDS["P3"] = "[M-11] 갈고리 연장 모델 86.7 mm < 5db 95 mm (KDS 4.5.4), 내면지름 66.5 mm < 공칭 기준 66.85 mm (0.35 mm 부족)."
for mk in ("B1", "B2"):
KDS[mk] = "[M-10] S25 굽힘: 모델 내면지름 87.6 mm < 공칭 기준 88.9 mm (1.3 mm 부족, 3.5db)."
for mk in ("B3", "B7", "B10"):
KDS[mk] = "[M-10] S19 굽힘: 모델 내면지름 66.6 mm < 공칭 기준 66.9 mm (0.3 mm 부족, 3.5db)."
# =============================================================== 5. 표 행 만들기
HEAD = ["번 호", "형 식", "직 경", "개 수", "A", "B", "C", "D", "E", "R", "X1(º)", "X2(º)", "J", "길 이(M)", "총길이(M)", "단위중량(KG/M)", "총중량(TON)", "총중량(할증)(TON)"]
KEYS = ["num", "shape", "dia", "n", "A", "B", "C", "D", "E", "R", "X1", "X2", "J", "L", "tot", "unit", "w", "wh"]
def surch(d): return 1.06 if int(d) >= 22 else 1.03
def build_model_table():
"""도면 행 순서 그대로 모델 행 생성. 반환 {tab: [row dict]} (cells=표시 문자열, vals=비교용)"""
out = {}
for tab in ("SD400", "SD500"):
rows = []; grp = [] # 직전 소계 이후 막대 행들
last = {"shape": None, "dia": None}
for r in drow[tab]:
if r["kind"] == "bar":
mk = r["mark"]; m = mod.get(mk)
cells = {"num": mk}; vals = {}
dcells = r["cells"]
shp = m["shape"] if m else r["cells"]["shape_v"]
dia = m["dia"] if m else r["cells"]["dia_v"]
cells["shape"] = '"' if shp == last["shape"] else shp
cells["dia"] = '"' if dia == last["dia"] else dia
last["shape"], last["dia"] = shp, 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}
dc = r["cells"]
if "소" in lab and "총" not 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": list(grp)})
grp = []
elif "겹이음" in lab:
cells.update(w="", wh="")
rows.append({"kind": "lap", "label": lab, "cells": cells, "vals": {}})
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["w"] = "%.3f" % w; cells["wh"] = "%.3f" % wh
if dc.get("tot"):
cells["tot"] = "%.3f" % tl
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):
"""막대 행 비교 → {col: 'same'|'diff'|'round'|...}, 자동 비고 목록"""
res = {}; auto = []
mk = dr["mark"]; 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:
r_ = same(dc.get(k, ""), mv.get(k), k)
res[k] = r_
if 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) <= 1e-9 + 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"
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}
wb = Workbook()
def write_table(ws, title, tables, extra=None, 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 [])
hr = ws.max_row + 1
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 > 18), horizontal=("center" if c <= 18 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=21).alignment = Alignment(vertical="top", wrap_text=True)
ws.cell(row=r_, column=22).font = Font(color="FF00FF")
ws.cell(row=r_, column=22).alignment = Alignment(vertical="top", wrap_text=True)
ws.append([])
widths = [10, 7, 7, 7, 10, 10, 10, 7, 7, 8, 8, 8, 6, 10, 11, 12, 11, 13]
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, ttl in (("SD400", "철 근 재 료 표 (SD400) — 3.dxf 원본 값 그대로"), ("SD500", "철 근 재 료 표 (SD500) — 3.dxf 원본 값 그대로")):
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((ttl, rows))
write_table(w1, "① 도면 재료표 재현 (운곡교 P1 완도방향, 도면 C0050205-027 3.dxf)", tabs1)
# ---- ② 모델 재료표
w2 = wb.create_sheet("②모델 재료표")
tabs2 = []
for tab, ttl in (("SD400", "철 근 재 료 표 (SD400) — 모델(운곡교p1.3dm) 기준"), ("SD500", "철 근 재 료 표 (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((ttl, rows))
write_table(w2, "② 모델 재료표 (모델 철근에서 추출: 개수·형상·치수 A~J·길이·총길이, 중량은 도면과 같은 단위중량·할증 적용)", tabs2)
# ---- ③ 대조·비고
w3 = wb.create_sheet("③대조·비고")
tabs3 = []
EXPORT = {"SD400": [], "SD500": []} # DXF 작성용 (make_bom_dxf.py 가 읽음)
stat = [] # 요약용
for tab, ttl in (("SD400", "SD400 — 모델 값 표시, 도면과 다른 칸은 색 + (도면: 값)"), ("SD500", "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 = [v for v in res.values()]
if "diff" in kind:
verdict = "차이"
elif "drawing_blank" in kind or "model_blank" in kind:
verdict = "도면 빈칸/모델 미추출"
elif "round" in kind:
verdict = "일치(반올림)"
else:
verdict = "일치"
fills[19] = {"차이": 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)
extra = ""
if mr["kind"] == "lap":
extra = "도면 값(%s / %s)만 있음. 겹이음 할증 산식이 도면에 없어 모델은 산출하지 않음." % (r["cells"].get("w", ""), r["cells"].get("wh", ""))
line[KEYS.index("w")] = "(도면: %s)" % r["cells"].get("w", "")
line[KEYS.index("wh")] = "(도면: %s)" % r["cells"].get("wh", "")
line += ["", extra, "", ""]
rows.append({"line": line, "fills": fills, "bold": True})
sres = {KEYS[ci - 1]: "diff" for ci in fills}
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": sres, "verdict": "", "auto": [],
"note": extra, "kds": ""})
tabs3.append((ttl, 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 if c.column > 19 else 1.0) / wd) + 1) if c.column > 19 else 1 for p in s.split("\n")) if c.column > 19 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]=도면↔모델, [D-n]=도면 내부(검토기록_최종.md).")
w3.cell(row=2, column=1).font = Font(italic=True)
# ---- ④ 요약
w4 = wb.create_sheet("④요약")
w4.append(["도면 재료표 ↔ 모델 재료표 차이 요약 (2026-10-16, 검토 미확정)"])
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, 150], 1):
w4.column_dimensions[get_column_letter(i)].width = w
# ---- ⑤ 코드 설명
w5 = wb.create_sheet("⑤비고코드설명")
w5.append(["코드", "구분", "내용"])
for c in [
("M-1", "도면↔모델", "C14 최하단 점 2개 모델이 350 mm 높음(근거 없음)"),
("M-2", "미확인", "C14 훅 방향 도면 표기 없음 (모델은 아래 가정)"),
("M-3", "기준↔모델", "C7·C8 굽힘 4곳 각진 연결 (내면반지름 미반영)"),
("M-4", "기준↔모델", "C1~C6 내면반지름 여유 거의 없음"),
("M-5", "도면↔모델", "C9~C13 길이 산식 +54.3 규칙이 변하는 치수에만 적용"),
("M-6", "도면↔모델", "S1·S2 높이 기준 (도면 대좌 상면 -50 ↔ 모델 코핑 상면 -50)"),
("M-7", "도면↔모델", "S1·S2 층 순서 (도면 S2 위 ↔ 모델 S1 위)"),
("M-8", "도면↔모델", "기초 교차 철근 간격 (도면 27~28 mm ↔ 모델 54~56 mm)"),
("M-9", "도면↔모델", "기초 B5 중심 73 아래(피복 58.5), B2·B6 다리 끝 높이"),
("M-10", "기준↔모델", "기초 S25·S19 굽힘 내면지름이 공칭 기준에 1.3/0.3 mm 부족"),
("M-11", "기준↔모델", "P3 갈고리 연장 86.7 < 5db 95, 내면지름 66.5 < 66.85"),
("M-12", "미확인", "P3 이음·P4/P5 커플러 각도 도면에 없음 (모델 가정)"),
("M-13", "도면↔모델", "UA1(73본, H22) 모델에 없음"),
("D-4/D-5", "도면 내부", "스터럽 폭 재료표 1.407 ↔ 단면 작도 1.160/1.740, 폐합근 1.106/1.686"),
("D-6", "도면 내부", "재료표 L 이 (최대+최소)/2 근사 → 총길이 과대"),
("D-7", "도면 내부", "C2·C5 L 12.081 ↔ 치수 합 12.082"),
("D-8", "도면 내부", "다리 각도 재료표 98°/96° ↔ 정면도 96.4°"),
("D-9", "도면 내부", "C7·C8 재료표 B·D·R·X·J 빈칸"),
("D-15~D-18", "도면 내부", "기초 점/선 높이 27~28 mm 차, B12 34↔28, B3 치수↔본수, B10 단 수"),
("D-20~D-23", "도면 내부", "P1/P2 상단 EL, 기둥 반지름, 링 R, P4·P5 @100↔@120"),
("E-01", "선행보고서", "기둥 띠철근 간격 → 구조계산서 확인 사항")]:
w5.append(list(c))
for i, w in enumerate([12, 14, 90], 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)", "호스트", "근거(src)"])
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"], 800), 1), us.get("host"), us.get("src")])
for c in range(1, 10):
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, 18, 20, 11, 18, 10, 70], 1):
w6.column_dimensions[get_column_letter(i)].width = w
w6.freeze_panes = "A2"
wb.move_sheet("④요약", offset=-3)
import json, os
jp = os.path.join(os.path.dirname(OUT), "추출데이터", "재료표_대조결과.json")
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))
@@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
"""운곡교 P1 재료표 도면(DXF) 대조본 작성.
도면 재료표(3.dxf)와 같은 칸 구성(열 위치·글자 높이·레이어)에 마크마다 두 줄(도면 / 모델)을 놓고,
다른 칸을 색으로 칠하고 오른쪽에 비고 열(비고 · 비고 3 KDS)을 붙인다.
입력: 추출데이터\재료표_대조결과.json (make_bom_compare.py 가 만든다), 3.dxf (스타일·레이어 상속)
사용: python -I make_bom_dxf.py <pylib경로> <대조결과.json> <출력.dxf>
"""
import sys, json, math
sys.path.insert(0, sys.argv[1])
import ezdxf
from ezdxf.enums import TextEntityAlignment
from ezdxf.lldxf import const
SRC = r"D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\3.dxf"
data = json.load(open(sys.argv[2], encoding="utf-8"))
OUT = sys.argv[3]
doc = ezdxf.readfile(SRC)
msp = doc.modelspace()
for e in list(msp): # 원본 도면 내용은 지우고 스타일·레이어·단위만 상속
msp.delete_entity(e)
if "KR_TBL" not in doc.styles:
doc.styles.add("KR_TBL", font="malgun.ttf")
LAYERS = {
"대조_다름(주황)": (30, (248, 203, 173)),
"대조_반올림(파랑)": (140, (221, 235, 247)),
"대조_빈칸미추출(노랑)": (50, (255, 242, 204)),
"대조_일치(초록)": (3, (226, 240, 217)),
"대조_비고": (7, None),
"대조_비고3_KDS(자홍)": (6, None),
"대조_표선": (7, None),
"대조_글자": (7, None),
}
for n, (c, _) in LAYERS.items():
if n not in doc.layers:
doc.layers.add(n, color=c)
TH = 150 # 표 글자 높이 (원본과 동일)
SUB = 360 # 한 줄 높이 (원본 행 간격)
XB = [1752, 2652, 3552, 4452, 5352, 6852, 8352, 9852, 11352, 12852, 14352, 15852, 17352, 18852, 20352, 21552, 22752, 23952, 25152]
KEYS = ["num", "shape", "dia", "n", "A", "B", "C", "D", "E", "R", "X1", "X2", "J", "L", "tot", "unit", "w", "wh"]
HEAD = ["번 호", "형 식", "직 경", "개 수", "A", "B", "C", "D", "E", "R", "X1(º)", "X2(º)", "J", "길 이", "총길이", "단위중량", "총중량", "총중량(할증)"]
UNITS = {"L": "(M)", "tot": "(M)", "unit": "(KG/M)", "w": "(TON)", "wh": "(TON)"}
XK = XB[0] - 1500 # '구분'(도면/모델) 열
XV0, XV1 = XB[-1], XB[-1] + 1600 # 판정 열
XN0, XN1 = XV1, XV1 + 16000 # 비고
XK3_0, XK3_1 = XN1, XN1 + 12000 # 비고 3
XLEFT, XRIGHT = XK, XK3_1
MCOL = 6 # ACI 자홍
def col_x(k):
i = KEYS.index(k)
return XB[i], XB[i + 1]
def txt(x, y, s, h=TH, layer="대조_글자", color=None):
if s is None or s == "":
return
t = msp.add_text(s, height=h, dxfattribs={"layer": layer, "style": "KR_TBL"})
t.set_placement((x, y), align=TextEntityAlignment.MIDDLE_CENTER)
if color is not None:
t.dxf.color = color
def mtxt(x0, ytop, width, s, h=110, layer="대조_비고", color=None):
if not s:
return
m = msp.add_mtext(s, dxfattribs={"layer": layer, "style": "KR_TBL", "char_height": h})
m.dxf.insert = (x0, ytop, 0)
m.dxf.width = width
m.dxf.attachment_point = 1 # 좌상단
if color is not None:
m.dxf.color = color
def line(x0, y0, x1, y1, layer="대조_표선", color=None):
ln = msp.add_line((x0, y0), (x1, y1), dxfattribs={"layer": layer})
if color is not None:
ln.dxf.color = color
def fill(x0, y0, x1, y1, lname):
rgb = LAYERS[lname][1]
h = msp.add_hatch(dxfattribs={"layer": lname})
h.set_solid_fill(color=LAYERS[lname][0])
h.rgb = rgb
h.paths.add_polyline_path([(x0, y0), (x1, y0), (x1, y1), (x0, y1)], is_closed=True)
def est_lines(s, width, h=110):
cw = h * 0.8
per = max(8, int((width - 200) / cw))
n = 0
for part in (s or "").split("\n"):
n += max(1, math.ceil(len(part) / per))
return n
STAT_LAYER = {"diff": "대조_다름(주황)", "round": "대조_반올림(파랑)", "drawing_blank": "대조_빈칸미추출(노랑)", "model_blank": "대조_빈칸미추출(노랑)"}
VERD_LAYER = {"차이": "대조_다름(주황)", "일치(반올림)": "대조_반올림(파랑)", "도면 빈칸/모델 미추출": "대조_빈칸미추출(노랑)", "일치": "대조_일치(초록)"}
# ------------------------------------------------------------------ 범례
y = 0.0
txt((XLEFT + XRIGHT) / 2 - 8000, y + 1900, "운 곡 교 P1 (완도방향) 철 근 재 료 표 — 도면(3.dxf) ↔ 모델(운곡교p1.3dm) 대조", h=300)
txt((XLEFT + XRIGHT) / 2 - 8000, y + 1450, "각 마크마다 위 줄 = 도면 재료표 값, 아래 줄 = 모델에서 뽑은 값. 다른 칸은 색으로 표시하고 오른쪽 비고 열에 내용을 적음 (검토 미확정, 2026-10-16).", h=130)
lx = XLEFT
for name, label in (("대조_다름(주황)", "주황 = 도면과 다름"), ("대조_반올림(파랑)", "파랑 = 반올림 범위(≤1 mm/본)"),
("대조_빈칸미추출(노랑)", "노랑 = 도면 빈칸 / 모델 미추출"), ("대조_일치(초록)", "초록 = 일치 (판정 열)")):
fill(lx, y + 700, lx + 500, y + 1100, name)
txt(lx + 600 + len(label) * 55, y + 900, label, h=130)
lx += 700 + len(label) * 115
txt(lx + 1800, y + 900, "자홍 글자 = KDS 24 14 21 기준 상이(비고 3)", h=130, color=MCOL)
def build_table(title, rows, ytop):
"""rows: JSON 행 리스트. 반환: 표 아래 y"""
y = ytop
txt((XB[0] + XB[-1]) / 2, y + 400, title, h=220)
# 헤더 2줄
hy0 = y
for i, hd in enumerate(HEAD):
x0, x1 = XB[i], XB[i + 1]
txt((x0 + x1) / 2, y - SUB / 2, hd)
if KEYS[i] in UNITS:
txt((x0 + x1) / 2, y - SUB * 1.5, UNITS[KEYS[i]])
txt((XK + XB[0]) / 2, y - SUB, "구분")
txt((XV0 + XV1) / 2, y - SUB, "판정")
txt((XN0 + XN1) / 2, y - SUB, "비 고 (칸별 차이 · 도면 내부/도면↔모델 코드)")
txt((XK3_0 + XK3_1) / 2, y - SUB, "비고 3 (KDS 24 14 21 기준 상이)", color=MCOL)
y -= SUB * 2
line(XLEFT, hy0, XRIGHT, hy0, layer="대조_표선")
line(XLEFT, y, XRIGHT, y)
body_top = y
for r in rows:
auto = " / ".join(r.get("auto", []))
note = r.get("note", "")
ntext = auto + ("\\P" if auto and note else "") + note
need1 = est_lines((auto + "\n" + note).strip("\n"), XN1 - XN0)
need3 = est_lines(r.get("kds", ""), XK3_1 - XK3_0)
H = max(2 * SUB, max(need1, need3) * 150 + 80)
half = H / 2.0
yt, ym, yb = y, y - half, y - H
# 칸 색 (도면·모델 두 줄 모두 같은 열에 칠함)
for k, st in r["res"].items():
if k in ("shape", "dia") and st == "same":
continue
lname = STAT_LAYER.get(st)
if lname and k in KEYS:
x0, x1 = col_x(k)
fill(x0, yb, x1, yt, lname)
if r.get("kind") == "bar":
vl = VERD_LAYER.get(r["verdict"])
if vl:
fill(XV0, yb, XV1, yt, vl)
# 글자
for i, k in enumerate(KEYS):
x0, x1 = XB[i], XB[i + 1]
cx = (x0 + x1) / 2
dtxt = r["d"].get(k, "")
mtxt_ = r["m"].get(k, "")
if r["kind"] == "bar" and k in ("shape", "dia"):
pass
if r["kind"] != "bar" and k == "num":
lab = r.get("label", "")
if r["kind"] == "sum":
txt((XB[7] + XB[8]) / 2, y - half / 2, "소 계")
txt((XB[7] + XB[8]) / 2, ym - half / 2, "소 계")
else:
short = lab.replace("총 계", "총 계")
txt((XB[0] + XB[11]) / 2, y - half / 2, short, h=130)
txt((XB[0] + XB[11]) / 2, ym - half / 2, short + " (모델)" if "겹이음" not in short else "(모델 미산출)", h=130)
continue
txt(cx, y - half / 2, dtxt)
txt(cx, ym - half / 2, mtxt_ if (mtxt_ != "" or dtxt == "") else "-")
txt((XK + XB[0]) / 2, y - half / 2, "도면")
txt((XK + XB[0]) / 2, ym - half / 2, "모델")
if r.get("kind") == "bar":
txt((XV0 + XV1) / 2, (yt + yb) / 2, {"차이": "차이", "일치(반올림)": "반올림", "도면 빈칸/모델 미추출": "빈칸/미추출", "일치": "일치"}.get(r["verdict"], r["verdict"]), h=120)
mtxt(XN0 + 100, yt - 50, XN1 - XN0 - 200, ntext)
mtxt(XK3_0 + 100, yt - 50, XK3_1 - XK3_0 - 200, r.get("kds", ""), layer="대조_비고3_KDS(자홍)", color=MCOL)
# 선
line(XLEFT, yb, XRIGHT, yb)
line(XLEFT, ym, XB[-1], ym, color=8)
y = yb
# 세로선
for x in [XK] + XB + [XV1, XN1, XK3_1]:
line(x, hy0, x, y)
line(XLEFT, y, XRIGHT, y, layer="대조_표선")
return y
y1 = build_table("철 근 재 료 표 (SD400)", data["SD400"], -1000.0)
y2 = build_table("철 근 재 료 표 (SD500)", data["SD500"], y1 - 2500.0)
doc.header["$EXTMIN"] = (XLEFT, y2, 0)
doc.header["$EXTMAX"] = (XRIGHT, 2500, 0)
doc.saveas(OUT)
print("saved", OUT, "y", y1, y2)
@@ -0,0 +1,250 @@
import sys; sys.path.insert(0,"./pylib")
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
# ---- reuse the 4-way rows written earlier (head, R) without re-saving that workbook
src = open('xl4.py', encoding='utf-8').read()
a = src.index("head=['번호'")
b = src.index("for j,h in enumerate(head,1):")
ns = {}
exec(src[a:b], ns)
R = [list(r) for r in ns['R']]
# ---- new rows (model-formula question) appended
R.append(['58','C9~C13','모델 길이 산식 규칙 (+54.3 적용 범위)','해당 없음','해당 없음','재료표 변하는 치수 = 도면 중심선 + 54.3 (선행보고서 7.2)','스터럽 높이 b·폐합근 길이 A 에는 +54.3 적용, 폭(스터럽 A, 폐합근 B)에는 미적용','확인 필요','C12 모델 폭 1106 (+54.3 = 1160)은 스터럽 폭과 같아짐. 폭에도 규칙을 적용해야 하는지 판단 필요 (적용 시 C12 +1.95 m, C13 +0.98 m)'])
R.append(['59','C1~C16','[단면도 (정면도 가운데, S=1:60, EL 43.555)] 철근 점·스터럽','해당 없음','B-B 오른쪽 절반(Y≥0)만 작도: 상면 3단 12점/행(깊이 248.2/348.2/448.2), C16 6점(104.5), C12·C13 행 3점(Y 317/843/1423, 깊이 793.7…), C7·C8 12점(하면 위 104.5), 스터럽 Y 290/870/1450(상·하 77.5)','해당 없음','B-B 와 같은 위치·높이 (모델이 B-B 값과 일치)','일치','단면도는 B-B 와 같은 단면을 전체 높이로 그린 것(같은 상면 EL 43.555, 깊이 3,144, 폭 3,100). 값은 B-B 와 0.3mm 이내로 같음'])
R.append(['60','C1~C16','[단면도] 점 위치·스터럽 높이·미확인 가로선','해당 없음','점 +4.5mm(C16 104.5), 스터럽 상·하 77.5, 상면 -100 전폭 선(EL 43.455) 1개: B-B 와 동일','해당 없음','B-B 와 동일 (100 안쪽)','도면 내부','D-1·D-3·D-12 와 같은 항목이 단면도에도 반복됨 (새 항목 없음)'])
R.append(['61','S1·S2','형상·길이·본수·총길이','D2 상세: 훅 700 (대좌 89 + 코핑 611), S1 길이 2.4 m(2.5 m 칸 안), S2 1.1 m','D2 단면 C-C: S1 가로선 10개(2.4 m), S2 세로선 21개(1.1 m)','S1 III H19 50본 A 2.400 B 700 L 3.800 / S2 105본 A 1.100 B 700 L 2.500','S1 50본(5격자×10) L 3,800, 총 190.0 m / S2 105본(5격자×21) L 2,500, 총 262.5 m','일치','총길이 재료표 190.000 / 262.500 과 정확히 일치'])
R.append(['62','S1·S2','간격 (D2 단면 C-C, 도면 좌표 ÷2)','해당 없음','S1: 112.5 / 125 x7 / 112.5 (1.2 m 칸), S2: 50, 75, 125 x18, 75 (2.5 m 칸)','해당 없음','S1: 113/125/124…/112, S2: 74, 125 x18…, 74 (회전 4.4° 반영)','일치','D2 상세는 S=1:30 이라 좌표가 2배로 그려져 있음 (치수 문자는 실제값)'])
R.append(['63','S1·S2','높이 기준 (S1 철근 중심)','D2 전열 상세: 대좌 상면에서 50 아래 (대좌 높이 139, 훅 아래로 89+611)','해당 없음','해당 없음','코핑 상면에서 50 아래 (대좌 아래, 예: z 43,648 = 코핑 EL 43.698 -50), 훅 700 아래','불일치','모델이 대좌 높이만큼(139~164mm) 낮음. 모델 src 가 "추론"으로 표기됨. 도면에서 해석 확인 필요'])
R.append(['64','S1·S2','층 순서 (위/아래)','D2 상세: S2 점이 S1 선보다 13.5mm 위 (작도 기호 오프셋)','해당 없음','해당 없음','S1 위 · S2 아래, 50mm 간격 (src "upper layer [추론]")','불일치','도면은 S2 가 위, 모델은 S1 이 위. 도면은 확정 표기 없음 → 확인 필요'])
R.append(['65','S1·S2','배치 대상 (전열·후열 / 대좌 개수)','D1 평면도: 3.100 폭에 S1 / S2 / S1 구간 표기, D2 상세는 전열 1개 (2.5 x 1.2)','해당 없음','S1 50 = 5x10, S2 105 = 5x21','5개 격자, 각 격자가 전열·후열 두 대좌를 가로지르는 2.4 m (Y -1288~1104)','미확인','D1 평면도의 S1/S2/S1 구간 표기와 D2 상세 1개의 대응이 도면에 명확하지 않음'])
R.append(['66','S1·S2','D2 전열 상세의 대좌 높이','D2 상세 대좌 139','해당 없음','해당 없음','전열(3500 kN) 대좌 164, 후열(2800 kN) 대좌 139','미확인','상세 제목은 전열인데 높이 139 는 모델 후열 값. 도면 내부 불일치 가능성'])
R.append(['67','B1~B8','[기초] 상·하면 철근 높이: 같은 철근을 정면도와 측면 단면도에서 비교','정면도(기초): 선(X방향 철근) B5 +2100, B6·B8 +150, B7 +250 / 점(Y방향) B1 +2073, B2·B4 +122, B3 +227.5','측면 단면도(기초): 선(Y방향) B1 +2100, B2·B4 +150, B3 +250 / 점(X방향) B5 +2127, B6·B8 +178, B7 +277','해당 없음','모델은 점 값 사용: B1 +2073, B5 +2127, B2·B4 +122, B6·B8 +178, B3 +227.5, B7 +277','불일치','같은 철근이 두 뷰에서 27~28mm(B3 22.5) 다르게 그려짐 (선=설계 중심, 점=접촉 철근 기호). 도면 내부 불일치, 모델은 점 값을 따름'])
R.append(['68','B1~B8','[기초] 교차하는 두 철근의 간격 (상면 B1/B5, 하면 B2·B4/B6·B8, 중앙 B3/B7)','두 뷰 모두 27~28mm (철근 지름 합의 반, 접촉)','측면 단면도도 27~28mm','해당 없음','상면 54, 하면 56, 중앙 49.5 (점 값을 서로 다른 뷰에서 가져와 간격이 2배)','불일치','모델이 도면 두 뷰 중 어느 쪽과도 맞지 않는 조합. 모델 확인·수정 필요'])
R.append(['69','B5, B6, B2','[기초] 상면 피복·다리 끝 높이','상면 선 100 아래, 다리 B6 +150~+2100 (끝 100 아래)','다리 B2 +150~+2100','다리 B=1.950','B5 중심 73 아래(순피복 58.5, 선행 E-06), B6 다리 끝 +2128 (72 아래), B2 다리 끝 +2072 (128 아래)','불일치','도면 선 기준 100 아래와 모델이 ±27mm 다름. 선행보고서 E-06 확인'])
R.append(['70','B12','[기초] 본수','우측 점 3+4+3+4=14 (z 550/950/1350/1750) → 전체 28','측면 단면도 B11 점도 28 구성','B12 34본 (L 7.800, A 7.600 B 100)','28본 (총 218.400 m)','불일치','재료표 34 ↔ 작도 28 (선행 E-05 확인, 모델은 작도를 따름). 재료표 총길이 265.200 ↔ 모델 218.400'])
R.append(['71','B3','[기초] 위치 치수 (중앙 줄)','점 38개 (오프셋 125~4375, 4575), 치수 줄 "100 / 450 / 33@250 = 8.250 / 450 / 100"','해당 없음','B3 38본','38본 (점과 동일)','불일치','치수(450 끝, 34+2=36본)와 점·재료표(38본)가 다름. 도면 내부, 모델은 점·재료표를 따름'])
R.append(['72','B10','[기초] 단 수와 치수','치수 "9@200=1.800" (10단 z 1950~150), 점은 9단 (z 1950~350)','측면도 점도 9단','B10 9본, A 9.198 B 7.644','9단 z 1950~350, 루프 9.198 x 7.644','불일치','치수는 10단인데 점·재료표는 9단. 점 위치 ±4593/±3818 ↔ 표 ±4599/±3822 (6/4mm)'])
R.append(['73','B9','[기초] 수량·배치','라벨 "H16 @500" (작도 위치 불명)','해당 없음','B9 H16 수량 칸 빈칸, L 2.155 총 1,062.415','493본 체크무늬 34x29 격자 (총길이 역산)','미확인','수량은 총길이/L 로 역산(선행 M-01). 도면에서 배치를 확인하지 못함'])
R.append(['74','B1~B8, B10~B12','[기초] 본수·형상·길이·간격','B1 점 19(우측)→38, B3 19→38, 하면 점 37→73, B12 14→28, B10 9단','B5·B7 점 16→32, B6+B8 점 31→61, B11 점 14→28','B1 38 / B2 38 / B3 38 / B4 35 / B5 32 / B6 32 / B7 32 / B8 29 / B10 9 / B11 28, 형상·A·B·L 표기','동일 본수·위치, 총길이 B1 304.0 / B2 437.0 / B3 304.0 / B4 266.0 / B5 305.6 / B6 417.6 / B7 305.6 / B8 265.35 / B10 303.156 / B11 261.8 m (재료표와 일치)','일치','하면 70@125=8.750 (+200 끝) = B2 38 + B4 35 교차, 상면 35@250 (+200 끝) = B1 38, 측면 58@125 / 29@250 = B6+B8 61 / B5 32'])
R.append(['75','B11, B12','[기초] 4단 높이와 위치','B12 점 z +550/950/1350/1750 (500 간격 교차), B11 선 +550/+1750','측면: B11 점 +550/950/1350/1750, B12 선 +550/+1750','B11 28 / B12 34','z 550/950/1350/1750, X ±375~±3875, Y ±250~±3250','일치','높이·위치는 도면과 모델 일치 (B12 본수는 행 70)'])
R.append(['76','B1~B12','[기초] 내면반지름 (굽힘 R 중심: S29 65.3, S25 56.3, S19 42.8, S16 24.0)','도면에 없음','도면에 없음','R 칸 빈칸','모델 파이프 직경 29/25/19/16 로 내면지름 101.6 / 87.6 / 66.6 / 32.0','기준 충족(여유 적음)','KDS 3.5db(공칭 28.6/25.4/19.1) = 100.1 / 88.9 / 66.9 : S25 1.3mm, S19 0.3mm 부족 (S29·S16 충족). 파이프 직경 대신 공칭을 쓰면 경계'])
R.append(['77','P1·P2','[기둥] 본수·형상·길이','D1 정면도: P1 S32 / P2 S32 Div.46','단면 C-C·D-D·E-E: 두 줄 링 S32 Div.46 (각 46본)','P1 XIV S32 46 (A 16.213 B 534 C 500 R 340 L 17.247), P2 XVI S32 46 (A 16.553 L 16.553)','P1·P2 각 46본, 길이 17,247 / 16,553, 총 793.365 / 761.438 m (재료표 793.362 / 761.438)','일치','P1 하단 훅: 정면도 호 R 340 (시작 z 26848 → 끝 z 26508, 수평 500) = 모델'])
R.append(['78','P1·P2','[기둥] 하단 높이 (기초 안)','P1 호 끝 z 26.508, P2 하단 z 26.508','해당 없음','해당 없음','하단 EL 26.508 (기초 하면 +300)','일치','정면도 점·선과 모델 일치'])
R.append(['79','P1·P2','[기둥] 상단 높이 (선행 E-02)','P1 상단 EL 43.011, P2 EL 40.411 (기둥 상단에서 끝)','해당 없음','P1 A 16.213 + 호 → 43.061, P2 A 16.553 → 43.061','P1·P2 모두 EL 43.061','불일치','도면 내부: 정면도 ↔ 재료표 (P1 50mm, P2 2,650mm). 모델은 재료표를 따름'])
R.append(['80','P1·P2','[기둥] 철근 중심 반지름 (선행 E-03)','선 P1 875 / P2 830, 치수 125·875·875·125 (P1 중심 875)','작도 점 P1 870.5 / P2 825.5','해당 없음','P1 870.5 / P2 825.5 (점 값)','불일치','도면 내부: 치수·선 ↔ 점 4.5mm (모델은 점, 띠철근 R 897 내면과 3.5mm 간섭 회피 쪽)'])
R.append(['81','P3','[기둥] 띠철근 단수·간격·형상','치수 36@300=10.800 (P5 상단 30.988부터, 상단 EL 41.788), 라벨 H19 @300, 링 점 36개','단면 E-E: H19 @300','P3 VI H19 36 (A 2.818 B 200 R 897 L 6.436)','36단 z 31288~41788 @300 (반원 2개씩 72), R 897, 설계 6.436','일치','좌우 반원 두 개로 구성, 이음은 단마다 90° 엇갈림 적용(도면에 기준 각도 없음 → 행 86)'])
R.append(['82','P3~P5','[기둥] 링 점 위치 반지름 (선행 E-04)','링 점 오프셋 902 (R 897 + 5)','해당 없음','R 897','R 897','불일치','도면 내부: 점 902 ↔ 표 897 (5mm, 4.5mm 안쪽 이동과 같은 유형), 모델은 표 897'])
R.append(['83','P4·P5','[기둥] 띠철근 간격 (선행 E-01, 계산서 확인)','치수 8@120=960 / 22@120=2.640 (총 17.203 사슬), 라벨 H19 @100','단면 C-C·D-D: H19 @100','P4 9본 (A 2.753 B 65 R 897 L 5.636), P5 22본 (A 5.571 B 65 R 897 L 5.636)','P4 9단 z 27388~28348 @120, P5 22단 z 28468~30988 @120 (치수를 따름)','불일치','도면 내부: 라벨 @100 ↔ 치수 @120 (재료표 본수 9·22는 @120과 일치). 구조계산서 확인 사항 — 모델은 @120 가정, 확정 아님'])
R.append(['84','P6','[기둥] 크로스타이 단수·개수·길이','선 EL 29.188 / 30.988 / 38.188 / 39.988 (1800 간격), 치수 8@1.800=14.400, 각 선 반길이 875','단면: 4방향 선(0/45/90/135°) 라벨 H16 @1.800','P6 III H16 36 (A 1.836 B 150 L 2.136)','9단 x 4방향 = 36본, z 27388~41788 @1800 (+17.5 링 위), 직선 1,788, 설계 2.136','일치(경미 차이)','정면도 선 반길이 875(총 1,750) ↔ 모델 직선 1,788 (링 반지름 894까지): 19mm/끝. 높이·본수 일치'])
R.append(['85','P1~P6','[기둥] 이음·갈고리 각도 기준 (P3 이음 엇갈림, P4·P5 커플러 위치, P1·P2 46분할 시작각)','"이음 부분이 엇갈리도록 배치" 노트만 있음','단면에 각도 표기 없음','해당 없음','P3 이음 0/180° 에서 단마다 90° 엇갈림, 커플러 90/270°, P1·P2 0°부터 7.826°','미확인','도면에 기준 각도가 없어 모델 값이 가정'])
R.append(['86','P3','[기둥] 갈고리 내면지름·연장 (모델 R 42.75, 구부림 142.4°, 연장 86.7)','도면에 없음','도면에 없음','B 200','내면지름 66.5 (3.5 x 모델 파이프 19) , 연장 86.7 (4.6db)','기준 미적용','KDS 24 14 21: 공칭 19.1 기준 내면지름 66.85 (0.35mm 부족), 갈고리 연장 5db = 95mm 보다 약 8mm 짧음 → 확인 필요 (P6 갈고리는 충족: 내면지름 32, 연장 126)'])
R.append(['87','받침','[받침] 대좌 평면·높이','D1 단면도: 후열 대좌 139.0 (전열은 169.4, 행 95)','D1 평면도: 받침 쌍 2.5 m 폭','D2 표: C6 1200, C3 2500(쌍), h1 전열 164 / 후열 139','대좌 1200 x 1250 (쌍 2500), 높이 164 / 139 (코핑 상면 위)','일치','회전 전열 4.4° / 후열 3.1° (행 97)'])
R.append(['88','받침','[받침] 몰탈 패드 평면·두께','D1 평면도 후열 3.1° 패드 (행 96)','해당 없음','D2 표: (A+200) x (B+200) = 900 x 840 / 990 (전열), 900 x 740 / 850 (후열), h2 50','900 x 840 / 990, 900 x 740 / 850, 두께 50','일치','패드 상면 = EL.X (행 91)'])
R.append(['89','받침','[받침] 교량받침 본체 A x B, 높이 h3','해당 없음','해당 없음','D2 표: A 700, B 640 / 790(교축방향 #3) / 540 / 650(교축방향 #8), h3 181','700 x 640 / 790 / 540 / 650, 높이 181','일치','교량받침 상세(S=1:20)의 A·B·h2·h3 정의와 일치'])
R.append(['90','받침','[받침] 솔플레이트 L x W, 두께, 경사','해당 없음','해당 없음','D2 표: L 800, W 740 / 890 / 640 / 750, T 30, S -1.901~-1.910%, T1 38 / T2 22','L 800, W 740 / 890 / 640 / 750, 두께 22.4~37.6 (평균 30)','일치(경미 차이)','T1·T2 정수 38/22 ↔ S 기준 37.6/22.4 : 0.4mm (표 반올림)'])
R.append(['91','받침','[받침] 패드 상면 EL.X (= 코핑 상면 + h1 + h2)','해당 없음','해당 없음','D2 표: 전열 43.625 / 43.697 / 43.768 / 43.840 / 43.912, 후열 43.600 / 43.672 / 43.744 / 43.816 / 43.887','43.625 / 43.697 / 43.769 / 43.841 / 43.912, 후열 43.600 / 43.672 / 43.744 / 43.816 / 43.887','일치(경미 차이)','#3, #4 만 1mm 차이 (코핑 계단 71.85mm 반올림)'])
R.append(['92','받침','[받침] 합계 높이 E = h1 + h2 + h3 + h4','해당 없음','해당 없음','D2 표: E 425 (전열), 400 (후열)','대좌 바닥~솔플레이트 평균 상면 425 / 400','일치','EL.X 정의는 E 가 아니라 코핑 상면 + h1 + h2 (선행 4.2)'])
R.append(['93','받침','[받침] 받침 중심 위치(Y) 와 구간 분할 C1·C2·C3, S1·S2','D1 평면도: 3.100 폭을 S1 / S2 / S1 로 표기','해당 없음','D2 표: C1 600 / 602, C2 1298, C3 2500, S1 852 / 950, S2 1298','받침 중심 Y -698 (전열) / +600 (후열) → 간격 1298, 코핑 반폭 1550 - 698 = 852, 1550 - 600 = 950','일치','S1 852 + S2 1298 + S1 950 = 3100 (받침 중심으로 나눈 구간). C1+C2+C1 = 2498 ↔ C3 2500 (표 2mm)'])
R.append(['94','받침','[받침] 받침 간격(X) 과 평면 위치','D1 평면도: 받침 간격 2.505, 후열 패드 중심 코핑 중심 +2548 / +5053','해당 없음','해당 없음','간격 2505, 후열 패드 중심 x +2548 / +5053','일치','평면도에서 후열 받침 4·5 번 위치 확인 (전열은 평면도에 다른 형식)'])
R.append(['95','받침','[받침] 전열 대좌 높이','D1 단면도 전열 대좌 높이 169.4','해당 없음','D2 표 h1 164','164','불일치','도면 내부: D1 169.4 ↔ D2 표 164 (5.4mm, 선행 E-10). 모델은 표를 따름'])
R.append(['96','받침','[받침] 후열 패드 평면 폭','D1 평면도 후열 패드 900 x 840 (전열과 같은 크기로 작도)','해당 없음','D2 표 (A+200) x (B+200) = 900 x 740 (B 540)','900 x 740','불일치','도면 내부: D1 840 ↔ D2 740 (100mm, 선행 E-11). 모델은 표를 따름'])
R.append(['97','받침','[받침] 평면 회전각 (곡선교)','D1 평면도 후열 받침 3.1° (전열은 형식이 달라 확인 못 함)','해당 없음','해당 없음','전열 4.4° / 후열 3.1°','미확인','후열 3.1° 일치, 전열 4.4° 미확인'])
R.append(['98','받침','[받침] 솔플레이트 경사 방향 (T1 / T2 어느 쪽이 두꺼운지)','교량받침 상세: 교축방향 화살표만 있고 T1/T2 위치 방향 불명','해당 없음','D2 표 S(%) 부호만 있음','모델이 가정 (선행 4.2)','미확인','도면에 방향 정의 보완 필요'])
R.append(['99','받침','[연단거리 검토] 받침 연단거리 (D2 교량받침 연단거리 검토)','D1 평면도·D2: 필요연단길이 s = 200 + 5.0L = 451.2mm, 적용거리 472.7mm','해당 없음','해당 없음','전열 #3 받침(790 x 700) 코핑 끝 연단거리 472.9 (전열 #1·2·4·5 는 478.7, 후열 586.1)','일치','적용거리 472.7 = 가장 작은 받침(#3, B 790, 4.4° 회전) 값과 0.2mm 차이. 필요 451.2 ≤ 적용 472.7'])
R.append(['100','받침','[연단거리 검토] 필요지지길이 N = 809mm ↔ 적용 1503.3mm','D2 검토표: N 809, 적용 1503.3','해당 없음','해당 없음','모델에서 대응하는 값을 특정하지 못함','미확인','적용 1503.3 이 어느 거리인지 도면에 정의가 없음 (필요 809 보다 큼)'])
R.append(['101','P1·P2','[기둥 단면 C-C·D-D·E-E] 46분할 시작각·반지름','해당 없음','점 92개 (각 46), 각도 0°부터 7.826° 간격, P1 870.5 / P2 825.5, 같은 각도에 정렬 (세 단면 동일)','S32 Div.46','46본씩, 0°부터 7.826°, P1 870.5 / P2 825.5','일치','선행 M-12 중 46분할 시작각은 확인됨 (P3 이음·커플러 각도는 단면에도 표기 없음)'])
R.append(['102','P3~P6','[기둥 단면] 띠철근 반지름과 크로스타이 길이','정면도 링 점 902, 크로스타이 선 반길이 875','단면: 띠철근 호 R 897.5, 크로스타이 4방향(0/45/90/135°) 길이 1795','R 897, P6 A 1.836 L 2.136','R 897, 크로스타이 직선 1788 (설계 2.136)','일치(경미 차이)','링 897 / 897.5 / 902 (0.5~5mm), 크로스타이 1795 ↔ 1788 (7mm)'])
R.append(['103','P3~P5','[기둥 단면] 단면별 띠철근 종류','해당 없음','C-C: P4 H19 @100, D-D: P5 H19 @100, E-E: P3 H19 @300','P4 9 / P5 22 / P3 36','P4 z 27388~28348, P5 28468~30988 (@120), P3 31288~41788 (@300)','일치','@100 은 선행 E-01 (치수는 @120, 계산서 확인)'])
R.append(['104','UA1','[기초 조립용 철근] 모델 반영 여부','해당 없음','D3 상세: 70° 삼각 지지, 500 수평 다리, 상단 200 꺾임, 길이 VAR','UA1 XVII H22 73본 (A 200, B 1.988, C 500, L 5.176 = 2B + A + 2C), 총 377.848 m, 1.149 t','모델에 없음 (선행 M-03: 모델 제외)','불일치','도면↔모델: 모델에 조립용 철근이 빠져 있음 (73본). 도면은 "배치 예시도"'])
R.append(['105','UA1','[기초 조립용 철근] 수량·배치','D3 설치위치 도면: 1.0 x 1.2 간격 예시도 (S=NONE), "실제 철근 배치 간격에 따라 위치는 변경"','압축 검토표: 기초 두께 H=2.0 → H19, 2.5·3.0 → H22 (기초 2.2 m 에 H22)','73본','-','미확인','수량 73본이 어떤 배치에서 나왔는지 도면에서 확인 불가 (예시도)'])
for r in R:
if r[0] == '21':
r[3] = '우측 끝 선 꺾임점 오프셋 C1 5941.6 / C2 5952.6 / C3 5964.6 (정면도)'; r[4] = '해당 없음'; r[6] = '5943.1 / 5953.9 / 5964.8 (모델 직선 끝 + 굽힘 접선)'; r[7] = '일치'; r[8] = '우측만 확인 (1.5mm 이내). 좌측은 정면도 미작도'
if r[0] == '97':
r[3] = 'D1 평면도: 전열 85.6° (= 4.4°), 후열 86.9° (= 3.1°)'; r[6] = '전열 4.4° / 후열 3.1°'; r[7] = '일치'; r[8] = '두 회전각 모두 평면도 선 방향과 일치'
if r[0] == '65':
r[3] = 'D1 평면도 S1/S2/S1 은 치수 이름 (표 S1·S2 칸 852/1298/950 과 같은 이름)이며 철근 마크가 아님'; r[8] = '구간 표기는 해소됨. 남은 문제: D2 상세(전열 1개, 2.5x1.2)가 전열 대좌만인지 대좌 쌍 전체인지 도면에 없음'
if r[0] == '57':
r[1] = 'C14'; r[3] = '해당 없음'; r[4] = 'B-B 상면 -100 전폭(2,900) 선 1개 (EL 43.455): 절단면 뒤쪽 C14 상단 연결근(x=+2913)이 선으로 보이는 것. 정면도 점은 EL 43.443'; r[6] = 'C14 상단 EL 43.442 (x=+2913, 1본)'; r[7] = '일치(경미 차이)'; r[8] = '해소: B-B 는 절단면 뒤쪽 철근도 그림. 점(43.443) ↔ 선(43.455) 12mm 는 점/선 표기 차이 (D-1 과 같은 유형)'
if r[0] == '79':
r[8] = '도면 내부: 정면도·D2 주철근 조립도(P1 2,600 위 / P2 기둥 상단까지) ↔ 재료표 (P1 50mm, P2 2,650mm). 모델은 재료표를 따름'
if r[0] == '66':
r[8] = '상세 제목은 전열인데 높이 139 는 후열 값 (모델 전열 164 / 후열 139). 도면 내부 불일치'
if r[0] == '47':
r[3] = '좌측 반쪽 정면도에 없음'; r[7] = '일치(간접)'; r[8] = '간접 확인: 재료표 B 범위 최솟값 1.376 = 모델 맨 왼쪽 스터럽 b 1,376.5 (중심선 높이 + 54.3 규칙). 좌우 대칭 배치는 가정'
if r[0] == '55':
r[8] = '추정: 도면 점 위치로는 최하단 점(x=±1913 EL 41.011, x=±2913 EL 41.511)은 훅을 아래로 하면 하면 밖으로 164 / 74mm 나가고, 상단 점(x=2913 EL 43.442)은 훅을 위로 하면 상면 밖으로 165mm 나감 → 하부 점은 위, 상부 점은 아래 훅으로 보임 (도면 직접 표기는 없음)'
if r[0] == '54':
r[8] = '모델이 최하단 점 2개를 350mm 올림 (하면과 훅 간섭 때문으로 추정, 근거 기록 없음). 훅을 위로 두면 도면 점 위치 그대로 들어가므로 올릴 필요가 없을 가능성 (행 55 추정)'
if r[0] == '65':
r[7] = '일치(간접)'; r[8] = '간접 확인: 받침 쌍 풋프린트 C3 2500 x C6 1200 = 메시 2.5 x 1.2 (대좌 1개 1250 x 1200 보다 큼), 재료표 50/105 = 5격자 x 10/21 → 모델의 "격자당 1장(쌍 전체)"과 일치. 상세 제목 "전열"은 보는 방향 표기로 추정'
if r[0] == '73':
r[7] = '일치(간접)'; r[8] = '간접 확인: 라벨 H16 @500 + 격자 34 x 29 (@250) 체크무늬 986 / 2 = 493 = 총길이 1,062.415 / L 2.155 와 정확히 일치. 수량 자체는 도면에 미기재'
if r[0] == '32':
r[8] = r[8] + ' (간접: 재료표 A = E = 650 으로 좌우 같음)'
if r[0] == '56':
r[8] = r[8] + ' (간접: 재료표 L 5.611 = A 5.261 + 350 한쪽 훅, 왼쪽 끝 훅과 일치)'
if r[0] == '53':
r[8] = r[8] + ' (간접: 좌측 18본 = 재료표 41본 - 우측 23본)'
if r[0] == '98':
r[6] = '교축방향(Y) 경사 1.905% (전열 쪽 두꺼움), X 성분 0.10~0.15% 는 회전각(4.4°/3.1°) 때문'; r[8] = '도면은 T1/T2 위치·광주방향과의 대응 표기 없음 → 모델 두꺼운 쪽(전열) 가정, 확인 필요'
# ---- classification: 구분
CAT = {}
for r in R:
CAT[r[0]] = '일치'
for n in ('1','2','3','22','37','39','41','16','18','29','35','44','49','50','52','66','60','67','70','71','72','79','80','82','83','95','96','0'):
CAT[n] = '도면 내부'
for n in ('54','58','63','64','68','69','104'):
CAT[n] = '도면↔모델'
for n in ('36','20','76','86'):
CAT[n] = '기준↔모델'
for n in ('47','55','65','73','85','98','100','105'):
CAT[n] = '미확인'
for n in ('47','65','73'):
CAT[n] = '간접 확인'
# ---- colours
def fill(c): return PatternFill('solid', fgColor=c)
HDR = fill('1F3864')
COL = {'일치': fill('C6EFCE'), '도면 내부': fill('FFF200'), '도면↔모델': fill('FF9933'), '기준↔모델': fill('FFC000'), '미확인': fill('E7E6E6'), '간접 확인': fill('BDD7EE')}
thin = Side(style='thin', color='999999'); BR = Border(left=thin, right=thin, top=thin, bottom=thin)
def put_header(ws, r0, head):
for j, h in enumerate(head, 1):
c = ws.cell(r0, j, h); c.font = Font(bold=True, color='FFFFFF'); c.fill = HDR
c.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True); c.border = BR
def put_rows(ws, r0, rows, catcol, h=48):
for i, row in enumerate(rows, 1):
for j, v in enumerate(row, 1):
c = ws.cell(r0 + i, j, v); c.border = BR
c.alignment = Alignment(vertical='center', wrap_text=True, horizontal='center' if j in (1, 2, catcol) else 'left')
cc = ws.cell(r0 + i, catcol); cc.fill = COL.get(row[catcol - 1], COL['미확인']); cc.font = Font(bold=True)
ws.row_dimensions[r0 + i].height = h
def widths(ws, ws_w):
for j, w in enumerate(ws_w, 1): ws.column_dimensions[get_column_letter(j)].width = w
wb = Workbook()
# ======== sheet 0 : legend / summary
s0 = wb.active; s0.title = '분류 안내'
s0['A1'] = '운곡교 P1 코핑 철근 C1~C16 : 도면 ↔ 모델 일치 검토 (불일치 종류별 분류)'; s0['A1'].font = Font(bold=True, size=14)
s0['A2'] = '목적: 도면(정면도·단면 A-A/B-B·재료표)과 라이노 모델이 일치하는지가 최우선. 도면 내부 불일치는 따로 분리. 작성 2026-10-15, 검토 미확정.'
s0.merge_cells('A2:E2'); s0['A2'].alignment = Alignment(wrap_text=True); s0.row_dimensions[2].height = 32
cnt = {}
for r in R: cnt[CAT[r[0]]] = cnt.get(CAT[r[0]], 0) + 1
leg = [['색', '구분', '뜻', '항목 수', '볼 시트'],
['주황', '도면↔모델', '도면들끼리는 같거나 도면에 정보가 없는데 모델이 다르게 만들어졌거나 가정한 것 (모델 확인·수정 대상)', cnt.get('도면↔모델', 0), '① 도면↔모델 불일치'],
['진한 노랑', '기준↔모델', '설계기준(KDS 24 14 21)에 모델이 맞는지 (도면에는 정보 없음)', cnt.get('기준↔모델', 0), '① 도면↔모델 불일치'],
['노랑', '도면 내부', '도면끼리 서로 다름 (정면도·단면·재료표 간). 모델은 그중 한쪽을 따름 (도면 정정 요청 대상)', cnt.get('도면 내부', 0), '② 도면 내부 불일치'],
['초록', '일치', '도면과 모델이 일치 (정상 차이·1mm 이하 포함)', cnt.get('일치', 0), '③ 전체 대조표'],
['연한 파랑', '간접 확인', '도면에 직접 표기가 없어 다른 값(재료표·형상·기하 제약)으로 추정 확인한 것 (도면 직접 확인은 아님)', cnt.get('간접 확인', 0), '③ 전체 대조표'],
['회색', '미확인', '확인하지 못했거나 도면에 정보가 없어 판단 불가', cnt.get('미확인', 0), '③ 전체 대조표']]
put_header(s0, 4, leg[0])
for i, row in enumerate(leg[1:], 1):
for j, v in enumerate(row, 1):
c = s0.cell(4 + i, j, v); c.border = BR; c.alignment = Alignment(wrap_text=True, vertical='center', horizontal='center' if j in (1, 2, 4) else 'left')
s0.cell(4 + i, 1).fill = COL[row[1]]
s0.row_dimensions[4 + i].height = 40
widths(s0, [12, 14, 80, 10, 24])
s0['A11'] = '판단 기준: "도면↔모델"은 모델이 도면과 다른 경우, "도면 내부"는 도면끼리 다른 경우. 도면끼리 다르면 모델은 반드시 한쪽과 달라지므로, 그 경우는 도면 내부로 분류하고 "모델이 따른 쪽"을 따로 적었다.'
s0.merge_cells('A11:E11'); s0['A11'].alignment = Alignment(wrap_text=True, vertical='top'); s0.row_dimensions[11].height = 44
s0['A12'] = '모델 파일: 운곡교p1.3dm (2026-10-14 저장본) / 도면: D1(교각도1 정면도·단면 A-A·B-B) D2 D3(재료표) — D:\\★Pier 자료\\심수석님 검토 파일\\철근 모델랑 운곡교P1_바이브코딩\\'
s0.merge_cells('A12:E12'); s0['A12'].alignment = Alignment(wrap_text=True); s0.row_dimensions[12].height = 32
# ======== sheet 1 : model vs drawing (priority)
M = [
['M-1','C14','최하단 점 2개 높이: x=1913 정면도 EL 41.011 / 모델 41.361, x=2913 정면도 EL 41.511 / 모델 41.861 (대칭 위치 x=-1913·-2913 도 같은 값)','정면도 점 ↔ 모델','모델이 350mm 높음','도면 점은 서로 일치(정면도·A-A 가로선), 모델만 다름','모델이 올린 근거 기록 없음. 정면도 점 위치대로면 훅(아래 가정 350)이 콘크리트 하면(EL 40.826 / 41.235) 아래로 나감 → 훅이 위쪽일 가능성. 모델 수정 또는 도면 확인 필요','도면↔모델'],
['M-2','C14','훅 방향: 도면에 표기 없음, 모델은 "아래"로 가정','도면 없음 ↔ 모델 가정','-','도면 정보 없음','x=+2913 상단 점(EL 43.442)은 위로 불가. M-1 과 함께 판단','미확인'],
['M-3','C7·C8','굽힘 4곳(호-다리 약 79° x2, 평면부-호 약 29° x2)이 각진 연결, 내면반지름 미반영','KDS 24 14 21 ↔ 모델','중심선 약 37mm 영향','C1~C6 모델은 필렛 적용됨(모델 내부에서도 불일치)','도면에는 반지름이 없음. KDS 내면지름 ≥ 3.5db 를 모델에 반영할지 결정 필요','기준↔모델'],
['M-4','C1~C6','내면반지름 여유 거의 없음: 모델 내면지름 약 101.5 ↔ KDS 3.5db=100.1(공칭 28.6) / 101.5(모델 파이프 직경 29 기준)','KDS 24 14 21 ↔ 모델','여유 0.4mm 이하','도면에는 반지름 없음','모델 파이프 직경이 29mm. 반지름을 키울지, 공칭 28.6 로 맞출지 결정 필요','기준↔모델'],
['M-6','S1·S2','철근 높이 기준: 도면 = 대좌 상면에서 50 아래(대좌 안), 모델 = 코핑 상면에서 50 아래(대좌 아래)','D2 전열 상세 ↔ 모델','139~164mm 낮음','도면 상세 1개뿐이며 대좌 높이 139','모델 src 가 "추론". 모델 수정 또는 도면 해석 확인 필요','도면↔모델'],
['M-7','S1·S2','층 순서: 도면 = S2 위, 모델 = S1 위 (50mm 간격)','D2 상세 ↔ 모델','순서 반대','도면에 확정 표기 없음 (작도 기호 위치만)','모델 src "upper layer [추론]". 확인 필요','도면↔모델'],
['M-8','B1~B8','기초 교차 철근 간격: 도면 27~28mm(접촉) ↔ 모델 54~56mm (점 값을 서로 다른 뷰에서 가져옴)','도면(정면도·측면도 모두) ↔ 모델','약 27mm','두 뷰 모두 27~28mm 로 일치','모델 B1/B5/B2·B4/B6·B8/B3/B7 높이 재조정 또는 근거 확인 필요','도면↔모델'],
['M-9','B5, B6, B2','기초 상면 피복·다리 끝: 도면 선 기준 상면 100 아래 ↔ 모델 B5 73, B6 다리 끝 72, B2 다리 끝 128 아래 (선행 E-06)','도면 선 ↔ 모델','±27mm','도면 두 뷰의 선이 같은 값(100)','M-8 과 함께 조정','도면↔모델'],
['M-10','B1~B12','기초 굽힘 내면반지름: 모델 S25 87.6mm, S19 66.6mm 는 KDS 공칭 기준(88.9 / 66.9)에 1.3 / 0.3mm 부족, S29·S16 은 충족','KDS 24 14 21 ↔ 모델','1.3mm','도면에 반지름 없음','파이프 직경 대신 공칭 직경으로 계산하거나 반지름 확대','기준↔모델'],
['M-11','P3','기둥 P3 갈고리: 모델 내면지름 66.5 (공칭 19.1 기준 66.85, 0.35mm 부족), 연장 86.7mm (KDS 5db=95mm 보다 약 8mm 짧음)','KDS 24 14 21 ↔ 모델','연장 약 8mm','도면에 반지름·연장 표기 없음','갈고리 형상(연장·반지름) 재검토 필요','기준↔모델'],
['M-12','P1~P6','기둥 철근 각도 기준 중 P3 이음 엇갈림·P4/P5 커플러 위치가 도면에 없어 모델이 가정 (46분할 시작각은 단면 C-C·D-D·E-E 에서 확인됨)','도면 없음 ↔ 모델 가정','-','도면에 기준 각도 없음','도면 보완 요청 또는 가정 명시','미확인'],
['M-13','UA1','기초 조립용 철근 UA1 (H22 73본, 377.848 m)이 모델에 없음 (선행 M-03). 도면은 배치 예시도, 수량은 재료표에서만 확정','재료표·D3 상세 ↔ 모델','73본','도면: 70° 삼각 지지 상세','모델 반영 여부 결정 (수량 근거 확인 필요)','도면↔모델'],
['M-5','C9~C13','모델 길이 산식에서 +54.3 규칙 적용 범위: 변하는 치수(스터럽 높이 b, 폐합근 길이 A)에는 적용, 폭(스터럽 A, 폐합근 B)에는 미적용','재료표 규칙(도면 중심선+54.3) ↔ 모델','C12 약 1.95 m, C13 약 0.98 m','재료표 폭 1.407 은 기준 불명','폭에도 적용해야 하는지 판단 필요','도면↔모델'],
]
s1 = wb.create_sheet('① 도면↔모델 불일치')
s1['A1'] = '① 도면 ↔ 모델 불일치 (최우선 확인) — 주황 / 진한 노랑'; s1['A1'].font = Font(bold=True, size=14)
s1['A2'] = '도면은 서로 일치(또는 정보 없음)하는데 모델이 다르게 만들어진 항목. 지금까지 C1~C16 코핑 철근에서 발견된 것은 아래 7건이며, 나머지 항목은 모델이 도면과 일치함(③ 시트).'
s1.merge_cells('A2:H2'); s1['A2'].alignment = Alignment(wrap_text=True); s1.row_dimensions[2].height = 32
h1 = ['번호', '마크', '내용', '비교', '차이 크기', '도면 쪽 상태', '확인·조치 필요 사항', '구분']
put_header(s1, 4, h1); put_rows(s1, 4, M, 8, 62); widths(s1, [8, 10, 62, 22, 18, 30, 62, 14])
# ======== sheet 2 : drawing-internal
D = [
['D-1','C1~C6, C16, C7·C8','단면 A-A·B-B 철근 점 그림이 치수·정면도보다 4.5mm 안쪽으로 그려짐 (상면 3단 104.5/204.5/304.5, C16, 하면 C7·C8 104.5)','단면 점 ↔ 단면 치수 · 정면도','4.5mm','모델은 치수·정면도(100)를 따름','치수 우선. 도면 정정 요청 사항 (선행보고서 E-06 유형)','도면 내부'],
['D-2','C7·C8','하면 피복: 단면 A-A 하면 치수 110 ↔ 단면 B-B 100 · 정면도 100','단면 A-A ↔ B-B · 정면도','약 9.5mm','모델은 100 (B-B·정면도 따름)','A-A 하면 치수만 다름. 오기 가능성, 확인 요청','도면 내부'],
['D-3','C9·C10·C11','스터럽 높이: 단면 A-A·B-B (상·하 77.5 안쪽) ↔ 정면도 (상·하 100 안쪽). 단면이 약 45mm 큼','단면 ↔ 정면도','약 45mm','모델은 정면도·재료표 B 를 따름 (재료표 B 도 정면도와 일치)','선행보고서에 없던 항목. 확인 필요','도면 내부'],
['D-4','C9·C10·C11','스터럽 폭: 재료표 A=1.407 (3루프 동일) ↔ 단면 1.160 / 1.740 / 1.160','재료표 ↔ 단면 A-A·B-B','3루프 합 4,221 ↔ 4,060','모델은 단면을 따름','선행보고서 E-07 확인','도면 내부'],
['D-5','C12·C13','폐합근 폭: 재료표 B=1.407 ↔ 단면 루프 폭 1106 / 1686 / 1106 (+54.3 = 1160/1740)','재료표 ↔ 단면 A-A·B-B','-','모델은 단면을 따름','선행보고서 E-08 확인','도면 내부'],
['D-6','C9·C11·C12','재료표 L 이 변하는 치수를 (최대+최소)/2 로 평균: 정면도·단면 형상으로 계산한 정확한 길이와 다름','재료표 ↔ 정면도·단면 형상','C9 -23.8 m, C11 -21.3 m, C12 -32.0 m','모델은 정확한 형상 길이','선행보고서는 폭 차이를 원인으로 봤으나 실제로는 평균 근사가 더 큼. 본별 길이 목록 요청','도면 내부'],
['D-7','C2·C5','재료표 L 12.081 ↔ A+B+C = 12.082','재료표 내부','1mm','모델은 12,082','반올림 오차로 추정','도면 내부'],
['D-8','C1~C6','다리 각도: 재료표 X1·X2 98°/96° ↔ 정면도 오른쪽 다리선 6.43°(=96.4°)','재료표 ↔ 정면도','0.4°','모델은 정면도(96.4°)를 따름. 왼쪽은 정면도 없어 97.6°는 미확인','재료표는 반올림 값으로 추정','도면 내부'],
['D-9','C7·C8','재료표 형상 VII 의 B·D·R·X1·X2·J 칸 빈칸 (선행보고서 M-04)','재료표 누락 ↔ 정면도(R 12,100 호)','-','모델은 정면도 R 12,100 사용','재료표 보완 요청. 정면도에서 해석 근거 확보','도면 내부'],
['D-10','C7·C8','정면도에 다리 650 구간 표기 없음 (C1 다리선과 겹침), 왼쪽 다리 정면도 없음 → 재료표 A = E = 650, L 13.994 로 간접 확인','정면도 정보 부족','-','재료표 650 = 모델 650','정면도로는 확인 불가','간접 확인'],
['D-13','S1·S2','D2 교좌면 보강 철근 상세("전열" 1개, 2.5x1.2)가 전열 대좌만 해당하는지 쌍 전체인지 도면에 없음 (D1 평면도 S1/S2/S1 은 치수 이름으로 해소됨)','D1 평면도 ↔ D2','-','모델은 5격자로 해석','도면 보완 요청','미확인'],
['D-14','S1·S2','D2 "전열" 상세의 대좌 높이 139 ↔ D2 표 전열(3500 kN) 대좌 164 (139 는 후열 값)','D2 상세 ↔ D2 표','25mm','모델은 표 (전열 164 / 후열 139)','도면 확인 요청','도면 내부'],
['D-15','B1~B8','기초: 같은 철근을 정면도·측면 단면도에서 점/선으로 그린 높이가 27~28mm 다름 (B1 2073↔2100, B5 2100↔2127, B2·B4 122↔150, B6·B8 150↔178, B7 250↔277, B3 227.5↔250)','정면도(기초) ↔ 측면 단면도(기초)','27~28mm','모델은 점 값을 사용','어느 값이 설계 중심인지 도면 확인 요청 (선 100 / 150 이 치수와 일치)','도면 내부'],
['D-16','B12','기초: 재료표 B12 34본 ↔ 작도 점 28 (선행 E-05 확인)','재료표 ↔ 정면도·측면도','6본 (46.8 m)','모델은 작도 28','재료표 정정 또는 도면 보완 요청','도면 내부'],
['D-17','B3','기초 중앙 줄 치수 33@250 + 450 (36본) ↔ 점·재료표 38본','정면도(기초) 치수 ↔ 점·재료표','2본','모델은 38','치수 정정 요청','도면 내부'],
['D-18','B10','기초 B10 치수 9@200=1.800 (10단) ↔ 점 9단·재료표 9본, 표 치수 ±4599/±3822 ↔ 점 ±4593/±3818','정면도(기초) 치수 ↔ 점·재료표','1단 / 6·4mm','모델은 9단·표 치수','치수 정정 요청','도면 내부'],
['D-19','B9','기초 B9 수량 빈칸 (선행 M-01), 배치 도면 불명','재료표 누락','-','모델은 총길이 역산 493본','재료표 수량 기입 요청','미확인'],
['D-20','P1·P2','기둥 주철근 상단: 정면도 P1 EL 43.011 / P2 EL 40.411(기둥 상단에서 끝) ↔ 재료표 둘 다 EL 43.061 (선행 E-02)','정면도 ↔ 재료표','P1 50mm, P2 2,650mm','모델은 재료표(43.061)','도면 정정 요청 (P2 정착 길이 영향 큼)','도면 내부'],
['D-21','P1·P2','기둥 주철근 반지름: 치수·선 P1 875 / P2 830 ↔ 작도 점 870.5 / 825.5 (선행 E-03)','치수·선 ↔ 점','4.5mm','모델은 점','치수 우선 여부 확인','도면 내부'],
['D-22','P3~P5','링 반지름: 정면도 점 902 / 단면 호 897.5 / 표 R 897 (선행 E-04)','점 ↔ 재료표','5mm','모델은 R 897','경미','도면 내부'],
['D-23','P4·P5','띠철근 라벨 H19 @100 ↔ 치수 @120 (재료표 9·22본은 @120과 일치, 선행 E-01) — 구조계산서 확인 사항','라벨 ↔ 치수','100 ↔ 120','모델은 @120','계산서에서 간격 확인 후 확정','도면 내부'],
['D-24','받침','전열 대좌 높이: D1 단면도 169.4 ↔ D2 표 h1 164 (선행 E-10)','D1 단면도 ↔ D2 표','5.4mm','모델은 표 164','도면 정정 요청','도면 내부'],
['D-25','받침','후열 패드 폭: D1 평면도 840 (전열 크기) ↔ D2 표 740 (선행 E-11)','D1 평면도 ↔ D2 표','100mm','모델은 표 740','도면 정정 요청','도면 내부'],
['D-11','C9','정면도 좌측 반쪽(C9 구간)이 그려지지 않음 → 재료표 B 최솟값 1.376 = 모델 맨 왼쪽 스터럽으로 간접 확인','정면도 정보 부족','-','모델은 우측과 대칭으로 작성','C9 위치·높이는 정면도로 확인 불가','간접 확인'],
['D-12','C14','단면 B-B 상면 -100 전폭 선 (EL 43.455) = 절단면 뒤쪽 C14 상단 연결근(x=+2913)이 선으로 보이는 것 (정면도 점 EL 43.443)','단면 B-B 선 ↔ 정면도 점','12mm','모델 EL 43.442','해소 (점/선 표기 차이)','일치'],
]
s2 = wb.create_sheet('② 도면 내부 불일치')
s2['A1'] = '② 도면 내부 불일치 (정면도·단면·재료표 서로 다름) — 노랑'; s2['A1'].font = Font(bold=True, size=14)
s2['A2'] = '도면끼리 다른 항목. 모델은 그중 한쪽을 따랐으므로 "모델이 따른 쪽"을 같이 적었다. 도면 정정 요청 또는 확인 요청용.'
s2.merge_cells('A2:H2'); s2['A2'].alignment = Alignment(wrap_text=True); s2['A2'].alignment = Alignment(wrap_text=True); s2.row_dimensions[2].height = 28
h2 = ['번호', '마크', '내용', '비교', '차이 크기', '모델이 따른 쪽', '조치', '구분']
put_header(s2, 4, h2); put_rows(s2, 4, D, 8, 56); widths(s2, [8, 14, 62, 26, 20, 34, 46, 12])
# ======== sheet 3 : full table
s3 = wb.create_sheet('③ 전체 대조표')
s3['A1'] = '③ 정면도 · 단면도(A-A, B-B) · 라이노 모델 · 재료표 4자 대조표 (C1~C16)'; s3['A1'].font = Font(bold=True, size=14)
s3['A2'] = '구분 열 색: 초록=일치 / 노랑=도면 내부 불일치 / 주황=도면↔모델 / 진한 노랑=기준↔모델 / 회색=미확인'
s3.merge_cells('A2:J2')
head3 = ['번호', '마크', '검토 항목', '정면도', '단면도(A-A / B-B)', '재료표(D3)', '라이노 모델', '판정', '비고 / 근거', '구분']
rows3 = []
for r in R:
rows3.append(list(r) + [CAT[r[0]]])
put_header(s3, 4, head3); put_rows(s3, 4, rows3, 10, 54); widths(s3, [6, 10, 28, 30, 34, 28, 34, 16, 52, 12])
s3.freeze_panes = 'D5'
# ======== sheet 4 : not reviewed
s4 = wb.create_sheet('④ 미검토 마크')
rows4 = [['항목', '내용', '비고', '상태']]
data4 = [['코핑 철근 간 간섭', '선행 보고서 4.3 (P6 크로스타이 교차, B5·B6 다리 겹침 등)', '사용자 지시로 제외', '제외'], ['좌측 반쪽', 'C9 좌측 스터럽, C7·C8 왼쪽 다리, C16 왼쪽 끝, C14 좌측 점 — 정면도에 좌측 반쪽이 그려지지 않음', '도면으로 확인 불가', '확인 불가']]
put_header(s4, 1, rows4[0])
for i, row in enumerate(data4, 1):
for j, v in enumerate(row, 1):
c = s4.cell(1 + i, j, v); c.border = BR; c.alignment = Alignment(wrap_text=True, vertical='center')
s4.cell(1 + i, 4).fill = COL['미확인']
widths(s4, [10, 18, 70, 10])
# ======== sheets 5/6 : height comparison + measurement basis (copied from the earlier height table)
from openpyxl import load_workbook
OLD = r'D:\★Pier 자료\심수석님 검토 파일\검토_운곡교P1\검토표_상면3단_높이비교.xlsx'
try:
ow = load_workbook(OLD)
for src_name, new_name in (('상면3단 높이 비교', '⑤ 상면3단 높이 비교'), ('측정 근거', '⑥ 측정 근거 좌표')):
so = ow[src_name]; sn = wb.create_sheet(new_name)
for row in so.iter_rows():
for c in row:
if c.value is None: continue
n = sn.cell(c.row, c.column, c.value); n.alignment = Alignment(wrap_text=True, vertical='center')
if c.fill is not None and c.fill.fgColor is not None and c.fill.fill_type:
n.fill = fill(c.fill.fgColor.rgb[-6:]) if isinstance(c.fill.fgColor.rgb, str) else n.fill
if c.font is not None and c.font.bold: n.font = Font(bold=True, color=(c.font.color.rgb[-6:] if c.font.color is not None and isinstance(c.font.color.rgb, str) else '000000'))
for col, dim in so.column_dimensions.items(): sn.column_dimensions[col].width = dim.width
for rng in so.merged_cells.ranges: sn.merge_cells(str(rng))
except Exception as ex:
print('height sheets not copied:', ex)
out = r'D:\★Pier 자료\심수석님 검토 파일\검토_운곡교P1\대조표_C1-C16_도면모델_분류.xlsx'
try:
wb.save(out)
except PermissionError:
out = out.replace('.xlsx', '_최종.xlsx'); wb.save(out)
print('saved', out, cnt)
@@ -0,0 +1,410 @@
# -*- coding: utf-8 -*-
"""운곡교 P1 철근재료표 재작성 (모델 기준) + 도면 재료표(3.dxf)와 비교 + 비고.
사용: python -I make_model_bom.py <pylib경로> <출력xlsx>
- pylib경로 : rhino3dm, openpyxl, ezdxf 가 설치된 폴더 (pip install --target)
- 입력(고정): 3.dxf(도면 재료표), 운곡교p1.3dm(모델)
"""
import sys, re, collections, statistics as st
sys.path.insert(0, sys.argv[1])
import ezdxf, rhino3dm
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
BASE = r"D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩"
DXF3 = BASE + r"\3.dxf"
M3DM = BASE + r"\운곡교p1.3dm"
OUT = sys.argv[2]
UNIT = {"32": 6.230, "29": 5.040, "25": 3.980, "22": 3.040, "19": 2.250, "16": 1.560} # kg/m (도면 표)
def surcharge(d): # 도면: D22 이상 6%, 미만 3%
return 1.06 if int(d) >= 22 else 1.03
# ---------------------------------------------------------------- 도면 재료표 파싱
doc = ezdxf.readfile(DXF3)
texts = []
for e in doc.modelspace():
if e.dxftype() == "TEXT":
texts.append((e.dxf.insert.x, e.dxf.insert.y, e.dxf.text))
elif e.dxftype() == "MTEXT":
texts.append((e.dxf.insert.x, e.dxf.insert.y, e.plain_text()))
COLS = [("num", 1900, 2500), ("shape", 2500, 3500), ("dia", 3500, 4400), ("n", 4400, 5300),
("A", 5300, 6700), ("B", 6700, 8200), ("C", 8200, 9700), ("D", 9700, 11200),
("E", 11200, 12700), ("R", 12700, 14200), ("X1", 14200, 15600), ("X2", 15600, 17100),
("J", 17100, 18800), ("L", 18800, 20500), ("tot", 20500, 21400)]
OFF = 23750
rowsraw = collections.defaultdict(dict)
for x, y, t in texts:
if y > 30300 or y < 20000:
pass
tab = "SD500" if x >= 25000 else "SD400"
xx = x - OFF if tab == "SD500" else x
for name, lo, hi in COLS:
if lo <= xx < hi:
rowsraw[(tab, round(y / 10) * 10)].setdefault(name, []).append(t.strip())
break
drw = {} # mark -> dict
order_draw = {"SD400": [], "SD500": []}
last = {}
for (tab, y), cells in sorted(rowsraw.items(), key=lambda kv: (kv[0][0], -kv[0][1])):
num = (cells.get("num") or [""])[0]
if not re.match(r"^[A-Z]{1,2}\d+(-\d+)?$", num) or y < 20000 or y > 30000:
continue
r = {"tab": tab, "mark": num}
for k in ("shape", "dia", "n", "A", "B", "C", "D", "E", "R", "X1", "X2", "J", "L", "tot"):
r[k] = (cells.get(k) or [None])[0]
for k in ("shape", "dia"):
if r[k] == '"':
r[k] = last[(tab, k)]
else:
last[(tab, k)] = r[k]
drw[num] = r
order_draw[tab].append(num)
def f(s):
try:
return float(s)
except Exception:
return None
for m, r in drw.items():
r["n_i"] = int(r["n"]) if r["n"] and r["n"].isdigit() else None
r["L_f"] = f(r["L"]) if r["L"] else None
r["tot_f"] = f(r["tot"]) if r["tot"] else None
r["dia_n"] = re.sub(r"\D", "", r["dia"] or "")
dims = []
for k in ("A", "B", "C", "D", "E", "R", "X1", "X2", "J"):
if r[k]:
dims.append("%s=%s" % (k, r[k]))
r["dims"] = " ".join(dims)
# ---------------------------------------------------------------- 모델
m3 = rhino3dm.File3dm.Read(M3DM)
lay = {l.Index: l.FullPath for l in m3.Layers}
def curve_len(c, n=3000):
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 += ((p.X - p0.X) ** 2 + (p.Y - p0.Y) ** 2 + (p.Z - p0.Z) ** 2) ** 0.5
p0 = p
return s
bars = collections.defaultdict(dict) # mark -> occ -> dict
for o in m3.Objects:
L = lay.get(o.Attributes.LayerIndex, "")
if not L.startswith("P1::철근"):
continue
g = o.Geometry
nm = o.Attributes.Name or ""
if isinstance(g, rhino3dm.Brep):
us = dict(o.Attributes.GetUserStrings())
mk = us.get("mark")
occ = us.get("occurrence")
b = bars[mk].setdefault(occ, {})
b.update(us=us, name=nm, layer=L)
elif L.endswith("중심선"):
p = nm.split("-")
if len(p) >= 4 and p[1] == "RC":
mk = p[2]
occ = "-".join(p[3:])
bars[mk].setdefault(occ, {})["cl"] = curve_len(g)
# ---------------------------------------------------------------- 마크별 모델 집계
def num(s):
try:
return float(s)
except Exception:
return 0.0
mod = {}
for mk, d in bars.items():
if mk is None:
continue
items = [b for b in d.values() if "us" in b]
Ls = [num(b["us"].get("design_length")) + num(b["us"].get("coupler_length")) for b in items]
cls = [b.get("cl") for b in items if b.get("cl") is not None]
pieces = len(items)
# P3·P4 는 반원 2조각 = 1링 (재료표 개수 = 링 수)
half = mk in ("P3", "P4")
n_tab = pieces // 2 if half else pieces
mod[mk] = dict(pieces=pieces, n=n_tab, tot=sum(Ls) / 1000.0, Lmean=(sum(Ls) / n_tab / 1000.0) if n_tab else 0,
Lmin=min(Ls) / 1000.0, Lmax=max(Ls) / 1000.0, cl=sum(cls) / 1000.0 if cls else None,
dia=items[0]["us"].get("dia"), shape=items[0]["us"].get("shape"), items=items, half=half)
# C9~C13 : 도면 폭(1.407) 기준으로 환산한 길이 -> 폭 영향 / (최대+최소)/2 근사 영향 분리
def width_effect(mk):
ex = 0.0
for b in mod[mk]["items"]:
w = num(b["us"].get("w_model"))
if w:
ex += 2 * (1407.0 - w)
return ex / 1000.0
# ---------------------------------------------------------------- 비고 사전
NOTE = {}
KDS = {}
def n_(mk, s):
NOTE[mk] = s
for mk in ("C1", "C4"):
n_(mk, "L 도면 14.490 ↔ 모델 14.489 (0.001 m, 각진 치수 반올림). [D-8] 도면 내부: 재료표 다리각 98°/96° ↔ 정면도 96.4°.")
for mk in ("C2", "C5"):
n_(mk, "[D-7] 도면 내부: 재료표 L 12.081 ↔ 치수 합 A+B+C 12.082 (모델 12.082, 1 mm). [D-8] 다리각 98°/96° ↔ 정면도 96.4°.")
for mk in ("C3", "C6"):
n_(mk, "개수·길이 일치. [D-8] 도면 내부: 재료표 다리각 98°/96° ↔ 정면도 96.4°.")
for mk in ("C7", "C8"):
n_(mk, "개수·길이 일치. [D-9] 도면 내부: 재료표 B·D·R·X·J 빈칸 → 모델은 정면도 R 12,100(하면 R 12,000 동심)·평면부 EL 40.511 로 작성. [D-10] 다리 650 은 정면도 겹침으로 확인 불가.")
n_("C9", "[D-4] 스터럽 폭 도면 재료표 1.407 ↔ 단면 작도 1.160/1.740/1.160 (모델은 작도값). [D-6] 재료표 L = (최대+최소)/2 근사 → 모델은 철근마다 개별 변길이. [M-5] 길이 산식 +54.3 규칙은 변하는 치수에만 적용.")
n_("C10", "[D-4] 스터럽 폭 1.407 ↔ 작도 1.160 (모델은 작도값). 변길이 b=2.998 고정이라 [D-6] 근사 영향 없음. [M-5] +54.3 규칙.")
n_("C11", "[D-4] 스터럽 폭 1.407 ↔ 작도 1.160/1.740/1.160. [D-6] (최대+최소)/2 근사 vs 모델 개별 변길이. [M-5] +54.3 규칙.")
n_("C12", "[D-5] 수평폐합근 폭 1.407 ↔ 작도 1.106/1.686/1.106 (모델은 작도값). [D-6] 길이 (최대+최소)/2 근사 vs 모델 개별. [M-5] +54.3 규칙.")
n_("C13", "[D-5] 수평폐합근 폭 1.407 ↔ 작도 1.106/1.686/1.106. [D-6] (최대+최소)/2 근사 vs 모델 개별. [M-5] +54.3 규칙.")
n_("C14", "개수·길이 일치. [M-1] 모델 최하단 점 2개(x=±1913, ±2913)가 도면보다 350 mm 높음(근거 기록 없음). [M-2] 훅 방향 도면 표기 없음 — 모델은 아래로 가정, 도면 점 위치로는 하부 점은 위·상부 점은 아래일 가능성(추정, 간접 확인).")
n_("C15", "개수·길이 일치.")
n_("C16", "개수·길이 일치. 훅 방향·정면도 좌측 끝은 도면 직접 표기 없음 (재료표 L 5.611 = 5.261 + 0.350 한쪽 훅으로 간접 확인).")
for mk in ("S1", "S2"):
n_(mk, "개수·길이 일치. [M-6] 높이 기준: 도면(D2 전열 상세)은 대좌 상면 -50 ↔ 모델은 코핑 상면 -50 (139~164 mm 낮음). [M-7] 층 순서: 도면은 S2 점이 S1 선 위 ↔ 모델은 S1 위·S2 50 mm 아래(모델 src '추론').")
n_("UA1", "[M-13] 모델에 없음(0본). 도면은 기초 조립용 '배치간격 예시도'(1.0×1.2 예시)라 수량 73본 근거 미확인. 도면 총길이 377.848 m 가 모델에는 빠짐.")
n_("P1", "개수·길이 일치(모델 17.247). [D-20] 주철근 상단: 정면도 EL 43.011 ↔ 재료표 EL 43.061 (모델은 재료표). [D-21] 반지름 치수·선 875 ↔ 점 870.5 (모델은 점).")
n_("P2", "개수·길이 일치. [D-20] 주철근 상단: 정면도 EL 40.411 ↔ 재료표 EL 43.061 (코핑 정착 2.65 m 차이, 모델은 재료표). [D-21] 반지름 830 ↔ 점 825.5 (모델은 점).")
n_("P3", "모델은 반원 2조각(72개)=1링, 재료표 개수 36 = 링 수로 환산. 링 길이 일치. [M-12] 단마다 90° 엇갈린 이음 각도는 도면에 기준 없음(모델 가정, 간접).")
n_("P4", "모델은 반원 2조각(18개)=1링(커플러 포함 L 5.636), 재료표 개수 9 = 링 수로 환산. [D-23] 라벨 H19 @100 ↔ 치수 8@120=960 (모델 @120 — 구조계산서 확인 사항, E-01). [M-12] 커플러 각도 도면에 없음.")
n_("P5", "개수·길이 일치(커플러 65 포함 L 5.636). [D-23] 라벨 @100 ↔ 치수 22@120=2,640 (모델 @120, 구조계산서 확인). [D-22] 링 점 902 ↔ R 897. [M-12] 커플러 각도 도면에 없음.")
n_("P6", "개수·길이 일치. 정면도 선 반길이 875 ↔ 모델 직선 894 (한쪽 19 mm, 경미).")
n_("B1", "개수·길이 일치. [M-8] 모델 교차 철근 간격 54~56 mm ↔ 도면 두 뷰 모두 27~28 mm(점 값을 서로 다른 뷰에서 가져옴: B1 2073 ↔ B5 2127). [D-15] 같은 철근을 점/선으로 그린 높이가 정면도·측면도에서 27~28 mm 다름.")
n_("B2", "개수·길이 일치. [M-9] 다리 끝 +2072 (도면 선 기준 +2100). [D-15] 점/선 높이 27~28 mm 차.")
n_("B3", "개수·길이 일치. [D-17] 도면 중앙 치수 33@250+450(36본) ↔ 점·재료표 38본 (모델 38).")
n_("B4", "개수·길이 일치. [M-8] 하면 교차 철근 간격 영향(B2·B4).")
n_("B5", "개수·길이 일치. [M-9] 모델 중심 73 아래(순피복 58.5 mm, 도면 표기 상면 100, 선행 E-06). [M-8] 교차 철근 간격.")
n_("B6", "개수·길이 일치. [M-9] 다리 끝 +2128 (도면 선 기준 +2100, 선행 E-06 피복 57.5). [M-8] 교차 철근 간격.")
n_("B7", "개수·길이 일치. [M-8] 교차 철근 간격.")
n_("B8", "개수·길이 일치.")
n_("B9", "[M-01 선행] 도면 재료표 개수 빈칸. 라벨 H16 @500 + 격자 34×29(@250) 체크무늬 986/2 = 493본을 총길이 1,062.415 ÷ L 2.155 로 역산(간접 확인) → 모델 493본, 총길이 일치. 기초 내 위치 확정은 도면에 없음.")
n_("B10", "개수·길이 일치. [D-18] 치수 9@200(10단) ↔ 점·재료표 9단 (모델 9단), 표 9.198×7.644 ↔ 점 ±4593/±3818.")
n_("B11", "개수·길이 일치. B11·B12 4단 높이(z 550)와 위치 일치.")
n_("B12", "[D-16] 도면 내부: 재료표 34본 ↔ 작도 28곳 (선행 E-05, 6본 과다 또는 작도 누락, 46.8 m). 모델은 작도 28본을 따름 → 6본 차이.")
KDS["C1"] = KDS["C2"] = KDS["C3"] = KDS["C4"] = KDS["C5"] = KDS["C6"] = "[M-4] 내면반지름 여유 거의 없음: 모델 내면지름 101.5 mm ↔ 3.5db 100.1/101.5 mm (H29, 표 4.5-1). 도면에 내면반지름 표기 없음."
KDS["C7"] = KDS["C8"] = "[M-3] 굽힘 4곳이 각진 연결(내면반지름 미반영) → 내면지름 3.5db(H29 100.1 mm) 미적용 (KDS 24 14 21 표 4.5-1)."
KDS["P3"] = "[M-11] 갈고리 연장 모델 86.7 mm < 5db 95 mm (KDS 4.5.4), 내면지름 66.5 mm < 공칭 기준 66.85 mm (0.35 mm 부족)."
KDS["B1"] = KDS["B2"] = "[M-10] S25 굽힘: 모델 내면지름 87.6 mm < 공칭 기준 88.9 mm (1.3 mm 부족, 3.5db)."
KDS["B3"] = KDS["B7"] = KDS["B10"] = "[M-10] S19 굽힘: 모델 내면지름 66.6 mm < 공칭 기준 66.9 mm (0.3 mm 부족, 3.5db)."
# ---------------------------------------------------------------- 엑셀
wb = Workbook()
ws = wb.active
ws.title = "철근재료표_모델기준"
thin = Side(style="thin", color="999999")
BR = Border(left=thin, right=thin, top=thin, bottom=thin)
HF = PatternFill("solid", fgColor="D9D9D9")
GREEN = PatternFill("solid", fgColor="E2F0D9")
ORANGE = PatternFill("solid", fgColor="FCE4D6")
YEL = PatternFill("solid", fgColor="FFF2CC")
GREY = PatternFill("solid", fgColor="EDEDED")
heads = ["번호", "형식", "직경", "도면 치수(재료표 표기)", "도면 개수", "도면 L(m)", "도면 총길이(m)",
"모델 개수", "모델 L 평균(m)", "모델 L 범위(m)", "모델 총길이(m)\n(재료표 방식: 각진 치수+커플러)", "모델 중심선 실총길이(m)\n(굽힘 반영)",
"개수 차이\n(모델−도면)", "총길이 차이(m)\n(모델−도면)", "판정", "단위중량\n(kg/m)", "도면 총중량(t)", "모델 총중량(t)",
"비고 (도면↔모델 차이 · 도면 내부 불일치)", "비고 3 (KDS 24 14 21 기준 상이)"]
ws.append(["운곡교 P1 (완도방향) 철근재료표 — 모델(운곡교p1.3dm) 기준 재작성 + 도면 재료표(3.dxf) 대비 비고"])
ws.append(["작성 2026-10-16 · 검토 미확정 · 모델 L = 철근 모델의 재료표식 길이(각진 치수 합 + 커플러), 중심선 실길이는 굽힘 반영값이라 짧음 · "
"[M-n]=도면↔모델, [D-n]=도면 내부, KDS=기준↔모델 (검토기록_최종.md 번호)"])
ws.append([])
ws.append(heads)
HR = 4
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)
def status(dn, mn, dt, mt, dL=None, mL=None):
if dn is None:
return "도면 개수 빈칸"
if mn != dn:
return "차이"
if dL is not None and mL is not None and abs(mL - dL) <= 0.0015: # 반올림 수준(≤1.5 mm/본)
return "일치" if abs(mt - dt) <= 0.0005 else "일치(경미)"
if abs(mt - dt) > 0.01:
return "차이"
return "일치(경미)" if abs(mt - dt) > 0.0005 else "일치"
summary_rows = [] # 소계 계산용
bw_rows = [] # C9~C13 차이 분해
all_rows = [] # 요약용 (마크, 판정, 개수차, 총길이차)
sections = [("SD400", "SD400 (코핑 C·S, 기둥 P3~P6, 기초 B9, UA1)"), ("SD500", "SD500 (기둥 P1·P2, 기초 B1~B8, B10~B12)")]
tot_all = {}
for tab, title in sections:
r0 = ws.max_row + 1
ws.append([title])
ws.cell(row=r0, column=1).font = Font(bold=True, size=12)
# 직경별 묶음 (도면 순서 유지, 직경 큰 순)
marks = order_draw[tab]
groups = collections.OrderedDict()
for mk in marks:
groups.setdefault(drw[mk]["dia_n"], []).append(mk)
grp_tot = []
for dia, mks in groups.items():
sd = drw[mks[0]]["dia"]
gd = gm = gdw = gmw = 0.0
gcl = 0.0
for mk in mks:
d = drw[mk]
mm = mod.get(mk)
mn = mm["n"] if mm else 0
mt = mm["tot"] if mm else 0.0
mcl = mm["cl"] if mm and mm["cl"] is not None else None
dn = d["n_i"]
dt = d["tot_f"] or 0.0
Lr = ("%.3f ~ %.3f" % (mm["Lmin"], mm["Lmax"])) if mm and abs(mm["Lmax"] - mm["Lmin"]) > 1e-6 else ""
stt = status(dn, mn, dt, mt, d["L_f"], mm["Lmean"] if mm else None)
note = NOTE.get(mk, "")
if mk in ("C9", "C10", "C11", "C12", "C13") and mm:
we = width_effect(mk)
diff = mt - dt
note += " ▶ 총길이 차이 %+.3f m = 폭 영향 %+.3f m + 근사·기타 %+.3f m." % (diff, -we, diff + we)
note += " 중심선 실길이는 겹침 이음(미작도, 길이만 반영)이 빠져 재료표 길이보다 짧음."
bw_rows.append((mk, dt, mt, diff, -we, diff + we))
if stt in ("차이", "도면 개수 빈칸") and mm is None:
pass
unit = UNIT[dia]
row = [mk, d["shape"], d["dia"], d["dims"], dn if dn is not None else "(빈칸)", d["L_f"], dt,
mn, round(mm["Lmean"], 3) if mm else 0, Lr, round(mt, 3), round(mcl, 3) if mcl is not None else None,
(mn - dn) if dn is not None else None, round(mt - dt, 3), stt, unit,
round(dt * unit / 1000, 3), round(mt * unit / 1000, 3), note, KDS.get(mk, "")]
ws.append(row)
all_rows.append((mk, stt, (mn - dn) if dn is not None else None, mt - dt))
rr = ws.max_row
fill = {"일치": GREEN, "일치(경미)": GREEN, "차이": ORANGE, "도면 개수 빈칸": YEL}[stt]
for c in range(1, len(heads) + 1):
cell = ws.cell(row=rr, column=c)
cell.border = BR
cell.alignment = Alignment(vertical="top", wrap_text=(c in (4, 19, 20)))
if c in (15,):
cell.fill = fill
ws.cell(row=rr, column=20).font = Font(color="FF00FF")
for c in (7, 11, 12, 14):
ws.cell(row=rr, column=c).number_format = "0.000"
for c in (17, 18):
ws.cell(row=rr, column=c).number_format = "0.000"
gd += dt; gm += mt; gcl += (mcl or 0.0)
gdw = gd * UNIT[dia] / 1000
gmw = gm * UNIT[dia] / 1000
sc = surcharge(dia)
grp_tot.append((sd, gd, gm, gcl, gdw, gmw, sc))
ws.append(["소 계", "", sd, "", "", "", round(gd, 3), "", "", "", round(gm, 3), round(gcl, 3), "", round(gm - gd, 3), "", UNIT[dia],
round(gdw, 3), round(gmw, 3), "할증 %d%% 적용 시: 도면 %.3f t / 모델 %.3f t" % (round((sc - 1) * 100), gdw * sc, gmw * sc), ""])
rr = ws.max_row
for c in range(1, len(heads) + 1):
ws.cell(row=rr, column=c).fill = GREY
ws.cell(row=rr, column=c).font = Font(bold=True)
ws.cell(row=rr, column=c).border = BR
for c in (7, 11, 12, 14, 17, 18):
ws.cell(row=rr, column=c).number_format = "0.000"
sd_ = sum(g[1] for g in grp_tot); sm_ = sum(g[2] for g in grp_tot); scl = sum(g[3] for g in grp_tot)
dw = sum(g[4] for g in grp_tot); mw = sum(g[5] for g in grp_tot)
dws = sum(g[4] * g[6] for g in grp_tot); mws = sum(g[5] * g[6] for g in grp_tot)
ws.append(["총 계 (%s)" % tab, "", "", "", "", "", round(sd_, 3), "", "", "", round(sm_, 3), round(scl, 3), "", round(sm_ - sd_, 3), "", "",
round(dw, 3), round(mw, 3), "할증 적용 총중량: 도면 %.3f t / 모델 %.3f t (모델 − 도면 %+.3f t)" % (dws, mws, mws - dws), ""])
rr = ws.max_row
for c in range(1, len(heads) + 1):
ws.cell(row=rr, column=c).fill = HF
ws.cell(row=rr, column=c).font = Font(bold=True)
ws.cell(row=rr, column=c).border = BR
for c in (7, 11, 12, 14, 17, 18):
ws.cell(row=rr, column=c).number_format = "0.000"
tot_all[tab] = (sd_, sm_, dw, mw, dws, mws)
ws.append([])
widths = [8, 7, 7, 34, 8, 9, 11, 8, 10, 14, 17, 17, 9, 12, 12, 9, 11, 11, 70, 48]
for i, w in enumerate(widths, 1):
ws.column_dimensions[get_column_letter(i)].width = w
ws.row_dimensions[HR].height = 48
ws.freeze_panes = "B5"
# ---------------------------------------------------------------- 시트 0: 요약
w0 = wb.create_sheet("요약", 0)
w0.append(["도면 재료표 ↔ 모델 재료표 차이 요약 (운곡교 P1, 2026-10-16 작성, 검토 미확정)"])
w0.append([])
w0.append(["구분", "마크", "도면 총길이(m)", "모델 총길이(m)", "차이(m)", "내용"])
dd = {m: drw[m]["tot_f"] or 0.0 for m in drw}
mm_ = {m: mod[m]["tot"] if m in mod else 0.0 for m in drw}
w0.append(["개수·길이 불일치", "UA1", dd["UA1"], mm_["UA1"], round(mm_["UA1"] - dd["UA1"], 3), "모델에 없음 (M-13). 도면은 배치 예시도, 73본 근거 미확인"])
w0.append(["개수·길이 불일치", "B12", dd["B12"], mm_["B12"], round(mm_["B12"] - dd["B12"], 3), "도면 재료표 34본 ↔ 작도·모델 28본 (D-16, 6본)"])
sw = sum(r[4] for r in bw_rows); so = sum(r[5] for r in bw_rows)
w0.append(["길이 불일치", "C9~C13", round(sum(r[1] for r in bw_rows), 3), round(sum(r[2] for r in bw_rows), 3), round(sum(r[3] for r in bw_rows), 3),
"스터럽·폐합근: 폭 1.407↔작도값 영향 %+.3f m + (최대+최소)/2 근사 등 %+.3f m (D-4/D-5/D-6)" % (sw, so)])
for r in bw_rows:
w0.append([" (세부)", r[0], round(r[1], 3), round(r[2], 3), round(r[3], 3), "폭 영향 %+.3f / 근사·기타 %+.3f" % (r[4], r[5])])
w0.append(["개수 표기 차이(합계 일치)", "P3·P4", "", "", 0, "모델은 반원 2조각 = 1링 (P3 72개, P4 18개), 재료표 개수는 링 수"])
w0.append(["개수 빈칸 보완", "B9", dd["B9"], mm_["B9"], round(mm_["B9"] - dd["B9"], 3), "도면 개수 빈칸 → 총길이 역산 493본(체크무늬)이 모델과 일치 (간접 확인)"])
w0.append([])
w0.append(["재료표 합계", "", "도면", "모델", "차이", ""])
for tab, v in tot_all.items():
w0.append([tab + " 총길이(m)", "", round(v[0], 3), round(v[1], 3), round(v[1] - v[0], 3), ""])
w0.append([tab + " 총중량(t)", "", round(v[2], 3), round(v[3], 3), round(v[3] - v[2], 3), "할증 적용: 도면 %.3f / 모델 %.3f" % (v[4], v[5])])
w0.append([])
w0.append(["그 외 마크는 개수·길이가 일치(반올림 ≤1.5 mm/본 포함)이며 위치·형상·굽힘 차이는 '철근재료표_모델기준' 시트 비고와 비고 3(KDS)에 적었음."])
for i, w in enumerate([24, 10, 16, 16, 12, 100], 1):
w0.column_dimensions[get_column_letter(i)].width = w
for c in range(1, 7):
w0.cell(row=3, column=c).font = Font(bold=True); w0.cell(row=3, column=c).fill = HF
w0.cell(row=1, column=1).font = Font(bold=True, size=12)
# ---------------------------------------------------------------- 시트 2: 비고 코드 설명
w2 = wb.create_sheet("비고코드설명")
w2.append(["코드", "구분", "내용"])
codes = [
("M-1", "도면↔모델", "C14 최하단 점 2개 모델이 350 mm 높음(근거 없음)"),
("M-2", "미확인", "C14 훅 방향 도면 표기 없음 (모델은 아래 가정)"),
("M-3", "기준↔모델", "C7·C8 굽힘 4곳 각진 연결 (내면반지름 미반영)"),
("M-4", "기준↔모델", "C1~C6 내면반지름 여유 거의 없음"),
("M-5", "도면↔모델", "C9~C13 길이 산식 +54.3 규칙이 변하는 치수에만 적용"),
("M-6", "도면↔모델", "S1·S2 높이 기준 (도면 대좌 상면 -50 ↔ 모델 코핑 상면 -50)"),
("M-7", "도면↔모델", "S1·S2 층 순서 (도면 S2 위 ↔ 모델 S1 위)"),
("M-8", "도면↔모델", "기초 교차 철근 간격 (도면 27~28 mm ↔ 모델 54~56 mm)"),
("M-9", "도면↔모델", "기초 B5 중심 73 아래(피복 58.5), B2·B6 다리 끝 높이"),
("M-10", "기준↔모델", "기초 S25·S19 굽힘 내면지름이 공칭 기준에 1.3/0.3 mm 부족"),
("M-11", "기준↔모델", "P3 갈고리 연장 86.7 < 5db 95, 내면지름 66.5 < 66.85"),
("M-12", "미확인", "P3 이음·P4/P5 커플러 각도 도면에 없음 (모델 가정)"),
("M-13", "도면↔모델", "UA1(73본, H22) 모델에 없음"),
("D-4/D-5", "도면 내부", "스터럽 폭 재료표 1.407 ↔ 단면 작도 1.160/1.740, 폐합근 1.106/1.686"),
("D-6", "도면 내부", "재료표 L 이 (최대+최소)/2 근사 → 총길이 과대"),
("D-7", "도면 내부", "C2·C5 L 12.081 ↔ 치수 합 12.082"),
("D-8", "도면 내부", "다리 각도 재료표 98°/96° ↔ 정면도 96.4°"),
("D-9", "도면 내부", "C7·C8 재료표 B·D·R·X·J 빈칸"),
("D-15~D-18", "도면 내부", "기초 점/선 높이 27~28 mm 차, B12 34↔28, B3 치수↔본수, B10 단 수"),
("D-20~D-23", "도면 내부", "P1/P2 상단 EL, 기둥 반지름, 링 R, P4·P5 @100↔@120"),
("E-01", "선행보고서", "기둥 띠철근 간격 → 구조계산서 확인 사항"),
]
for c in codes:
w2.append(list(c))
for i, w in enumerate([12, 14, 90], 1):
w2.column_dimensions[get_column_letter(i)].width = w
for c in range(1, 4):
w2.cell(row=1, column=c).font = Font(bold=True); w2.cell(row=1, column=c).fill = HF
# ---------------------------------------------------------------- 시트 3: 모델 개별 철근
w3 = wb.create_sheet("모델_개별철근")
w3.append(["마크", "직경", "형식", "발생(occurrence)", "모델 L(각진+커플러, mm)", "커플러(mm)", "중심선 실길이(mm)", "호스트", "근거(src)"])
for mk in sorted(mod, key=lambda s: (re.sub(r"\d", "", s), int(re.sub(r"\D", "", s) or 0))):
for occ, b in sorted(bars[mk].items()):
if "us" not in b:
continue
us = b["us"]
w3.append([mk, us.get("dia"), us.get("shape"), occ, round(num(us.get("design_length")) + num(us.get("coupler_length")), 3),
num(us.get("coupler_length")), round(b["cl"], 1) if b.get("cl") is not None else None, us.get("host"), us.get("src")])
for c in range(1, 10):
w3.cell(row=1, column=c).font = Font(bold=True); w3.cell(row=1, column=c).fill = HF
for i, w in enumerate([8, 7, 7, 18, 20, 11, 18, 10, 70], 1):
w3.column_dimensions[get_column_letter(i)].width = w
w3.freeze_panes = "A2"
wb.save(OUT)
print("saved", OUT)
for tab, v in tot_all.items():
print(tab, "도면 총길이 %.3f / 모델 %.3f | 도면 t %.3f(할증 %.3f) / 모델 t %.3f(할증 %.3f)" % (v[0], v[1], v[2], v[4], v[3], v[5]))
print("drawing marks:", len(drw), "model marks:", len(mod))
miss = [m for m in drw if m not in mod]
extra = [m for m in mod if m not in drw]
print("도면에만:", miss, "모델에만:", extra)
@@ -0,0 +1,51 @@
import sys, ezdxf, matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, FancyArrowPatch
from ezdxf.addons.drawing import RenderContext, Frontend
from ezdxf.addons.drawing.matplotlib import MatplotlibBackend
plt.rcParams['font.family']='Malgun Gothic'
Y='#FFE600'
d=ezdxf.readfile('1.dxf'); ms=d.modelspace()
fig=plt.figure(figsize=(18,11),dpi=100,facecolor='#1e2228')
ax=fig.add_axes([0.01,0.47,0.98,0.50])
Frontend(RenderContext(d),MatplotlibBackend(ax)).draw_layout(ms)
ax.set_xlim(39350,46350);ax.set_ylim(23100,24780)
ax.set_title('도면 D1 단면 A-A (노란색 = 검토 표기)',color=Y,fontsize=14)
ax.add_patch(Rectangle((43070,24340),3030,300,fill=False,ec=Y,lw=1.5,ls='--'))
ax.add_patch(Rectangle((43070,24545),3030,100,fill=False,ec=Y,lw=2.5))
ax.text(46150,24590,'①',color=Y,fontsize=18,fontweight='bold',va='center')
ax.text(46150,24400,'상면 3단',color=Y,fontsize=11,va='center')
ax.plot([40100,43070],[24591.5,24591.5],color=Y,lw=1.4,ls=':')
ax.text(40160,24625,'②',color=Y,fontsize=18,fontweight='bold',va='bottom')
ax.plot([43040,43200,43200,43040,43040],[24545,24545,24705,24705,24545],color=Y,lw=2)
ax.text(42960,24730,'③ 아래 확대',color=Y,fontsize=11,ha='right',va='bottom')
ax2=fig.add_axes([0.03,0.02,0.36,0.42])
Frontend(RenderContext(d),MatplotlibBackend(ax2)).draw_layout(ms)
ax2.set_xlim(43040,43200);ax2.set_ylim(24545,24705)
for sp in ax2.spines.values(): sp.set_color(Y); sp.set_linewidth(2.5)
ax2.set_xticks([]);ax2.set_yticks([])
ax2.plot([43040,43200],[24591.5,24591.5],color=Y,lw=1.6,ls='--')
ax2.plot([43120.3],[24587.0],'o',color=Y,ms=5)
ax2.annotate('',xy=(43172,24591.5),xytext=(43172,24587.0),arrowprops=dict(arrowstyle='<->',color=Y,lw=2))
ax2.text(43176,24577,'4.5mm',color=Y,fontsize=15,fontweight='bold',va='center')
ax2.annotate('',xy=(43070,24691.5),xytext=(43070,24591.5),arrowprops=dict(arrowstyle='<->',color=Y,lw=1.8))
ax2.text(43076,24640,'치수 100',color=Y,fontsize=13,va='center')
ax2.text(43044,24700,'콘크리트 상면 y=24691.5',color=Y,fontsize=10,va='top')
ax2.text(43126,24560,'작도 점 y=24587.0',color=Y,fontsize=10,va='top')
ax2.set_title('③ 좌상단 모서리 확대 (160×160mm)',color=Y,fontsize=13)
fig.text(0.42,0.43,"""① 상면 3단 철근(C1·C2·C3 / C4·C5·C6)의 작도 점
치수 100 / 200 / 300 → 작도 104.5 / 204.5 / 304.5
② 치수 100 의 기준선 (상면 24691.5 - 100 = 24591.5)
③ 작도 점 24587.0 은 그 기준선보다 4.5mm 아래
(하면 쪽도 같은 현상: 기준 23269.3 / 작도 23264.7, 4.6mm)
무슨 뜻인가
· 치수가 맞고 점 그림이 4.5mm 어긋난 것 → 치수 우선
· 모델은 치수 100 을 따름 → 도면 점과 4.5mm 차이
· 철근 반지름(14.3mm)보다 작아 시공상 영향 없음
· 보고서 E-06(기초 상면 피복 표기 100 vs 작도 2,127)과
같은 "치수 vs 작도 점" 불일치 유형""",
color=Y,fontsize=14,va='top',ha='left',linespacing=1.55)
out=r'D:\Rhino-MCP\rhino-mcp-main\Claude outputs\C1_피복_작도오차_표기.png'
fig.set_size_inches(18,11); fig.savefig(out,dpi=100,facecolor=fig.get_facecolor()); print(out)
@@ -0,0 +1,22 @@
import ezdxf, sys, math
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
cnt=defaultdict(list)
def add(a,b,layer):
L=math.hypot(b[0]-a[0],b[1]-a[1])
if 300<L<1500:
ang=round(math.degrees(math.atan2(b[1]-a[1],b[0]-a[0]))%90,1)
if abs(ang)>0.3 and abs(ang-90)>0.3: cnt[(layer,ang)].append((round((a[0]+b[0])/2),round((a[1]+b[1])/2),round(L)))
for e in ms:
t=e.dxftype()
if t=='LINE':
a,b=e.dxf.start,e.dxf.end
if 12500<min(a.x,b.x) and max(a.x,b.x)<18800 and 25800<min(a.y,b.y) and max(a.y,b.y)<30300: add((a.x,a.y),(b.x,b.y),e.dxf.layer)
elif t=='LWPOLYLINE':
p=[(q[0],q[1]) for q in e.get_points()]
for i in range(len(p)-1):
if all(12500<x<18800 and 25800<y<30300 for x,y in (p[i],p[i+1])): add(p[i],p[i+1],e.dxf.layer)
for k in sorted(cnt):
v=cnt[k]
if len(v)>=2: print(k,len(v),sorted(v)[:4])
@@ -0,0 +1,12 @@
import sys, ezdxf, matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from ezdxf.addons.drawing import RenderContext, Frontend
from ezdxf.addons.drawing.matplotlib import MatplotlibBackend
f,x0,x1,y0,y1,out=sys.argv[1],*map(float,sys.argv[2:6]),sys.argv[6]
d=ezdxf.readfile(f); ms=d.modelspace()
w=x1-x0;h=y1-y0
fig=plt.figure(figsize=(16,16*h/w),dpi=110); ax=fig.add_axes([0,0,1,1])
Frontend(RenderContext(d),MatplotlibBackend(ax)).draw_layout(ms)
ax.set_xlim(x0,x1);ax.set_ylim(y0,y1)
fig.set_size_inches(16,16*h/w); fig.savefig(out,dpi=120)
@@ -0,0 +1,22 @@
import ezdxf, sys
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
CX=30133.6
rows=defaultdict(list)
for e in ms:
if e.dxftype()=='CIRCLE' and e.dxf.radius==27:
c=e.dxf.center
if 28500<c.x<31800 and 16400<c.y<19700: rows[round(c.y,1)].append(round(c.x-CX))
for y in sorted(rows,reverse=True):
print(y,'depth below top %.1f'%(19659.3-y),'n',len(rows[y]),'Y',sorted(rows[y]))
print('stirrup lines (CS-RBAR-MAJR) in 단면도 box')
V=defaultdict(list);H=defaultdict(list)
for e in ms:
if e.dxftype()=='LINE' and e.dxf.layer=='CS-RBAR-MAJR':
a,b=e.dxf.start,e.dxf.end
if 28550<min(a.x,b.x) and max(a.x,b.x)<31750 and 16400<min(a.y,b.y) and max(a.y,b.y)<19700:
if abs(a.x-b.x)<0.6: V[round(a.x-CX,1)].append((round(min(a.y,b.y),1),round(max(a.y,b.y),1)))
elif abs(a.y-b.y)<0.6: H[round(a.y,1)].append((round(min(a.x,b.x)-CX),round(max(a.x,b.x)-CX)))
for k in sorted(V): print(' V Y',k,V[k][:2])
for k in sorted(H,reverse=True): print(' H y',k,'depth %.1f'%(19659.3-k),H[k][:3])
@@ -0,0 +1,22 @@
import ezdxf, sys
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
X0,X1,Y0,Y1=26000,36500,9000,22200
rows=defaultdict(list)
for e in ms:
if e.dxftype()=='CIRCLE' and e.dxf.radius==27:
c=e.dxf.center
if X0<c.x<X1 and Y0<c.y<Y1: rows[round(c.y,1)].append(round(c.x,1))
ys=sorted(rows,reverse=True)
print('rows:',[(y,len(rows[y])) for y in ys])
# concrete box lines near the coping
H=[];V=[]
for e in ms:
if e.dxftype()=='LINE' and e.dxf.layer=='CS-CONC-MAJR':
a,b=e.dxf.start,e.dxf.end
if X0<min(a.x,b.x) and max(a.x,b.x)<X1 and Y0<min(a.y,b.y) and max(a.y,b.y)<Y1:
if abs(a.y-b.y)<0.6 and abs(a.x-b.x)>800: H.append((round(a.y,1),round(min(a.x,b.x),1),round(max(a.x,b.x),1)))
if abs(a.x-b.x)<0.6 and abs(a.y-b.y)>800: V.append((round(a.x,1),round(min(a.y,b.y),1),round(max(a.y,b.y),1)))
print('H',sorted(set(H),reverse=True)[:12])
print('V',sorted(set(V))[:12])
@@ -0,0 +1,23 @@
import ezdxf, sys
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
d=ezdxf.readfile(r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'); ms=d.modelspace()
def run(name,x0,x1,y0,y1,cx,topy,boty):
V=defaultdict(list);H=defaultdict(list)
for e in ms:
if e.dxftype()=='LINE' and e.dxf.layer=='CS-RBAR-MAJR':
a,b=e.dxf.start,e.dxf.end
if not(x0<min(a.x,b.x) and max(a.x,b.x)<x1 and y0<min(a.y,b.y) and max(a.y,b.y)<y1): continue
if abs(a.x-b.x)<0.6: V[round(a.x,1)].append((round(min(a.y,b.y),1),round(max(a.y,b.y),1)))
elif abs(a.y-b.y)<0.6: H[round(a.y,1)].append((round(min(a.x,b.x),1),round(max(a.x,b.x),1)))
print('=====',name,'center x',cx,'top',topy,'bottom',boty)
print('vertical lines (Y from centre -> y range, inset from top/bottom):')
for x in sorted(V):
for y0_,y1_ in sorted(V[x]):
print(' Y %+.1f'%(x-cx),'y',y0_,'~',y1_,'| 상단 %.1f 아래, 하단 %.1f 위'%(topy-y1_,y0_-boty))
print('horizontal lines:')
for y in sorted(H,reverse=True):
for a,b in sorted(H[y]):
print(' y',y,'Y',round(a-cx,1),'~',round(b-cx,1),'| top-%.1f bot+%.1f'%(topy-y,y-boty))
run('A-A',43000,46100,23200,24680,44570.3,24691.5,23159.7)
run('B-B',43100,46300,10450,13620,44735.4,13624.2,10480.2)
@@ -0,0 +1,78 @@
import ezdxf, sys, math
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
SRC = r'D:\★Pier 자료\심수석님 검토 파일\철근 모델랑 운곡교P1_바이브코딩\1.dxf'
d = ezdxf.readfile(SRC)
ms = d.modelspace()
REG = tuple(float(v) for v in sys.argv[1:5]) # x0 x1 y0 y1 : region for dots + bubbles
CX = float(sys.argv[5]) # section centre x (to convert x to Y)
x0, x1, y0, y1 = REG
dots = []
bub = []
lines = []
txt = []
for e in ms:
t = e.dxftype()
if t == 'CIRCLE':
c = e.dxf.center
if e.dxf.radius == 27 and x0 < c.x < x1 and y0 < c.y < y1:
dots.append((c.x, c.y))
if e.dxf.radius == 240 and x0 - 4000 < c.x < x1 + 4000 and y0 - 1500 < c.y < y1 + 6000:
bub.append((c.x, c.y))
elif t == 'LINE' and e.dxf.layer in ('CS-DIML', 'CS-SYMB-LIN3', 'CS-SYMB'):
a, b = e.dxf.start, e.dxf.end
if x0 - 5000 < min(a.x, b.x) and max(a.x, b.x) < x1 + 5000 and y0 - 1500 < min(a.y, b.y) and max(a.y, b.y) < y1 + 6500:
lines.append(((a.x, a.y), (b.x, b.y)))
elif t == 'TEXT':
p = e.dxf.insert
if x0 - 5000 < p.x < x1 + 5000 and y0 - 1500 < p.y < y1 + 6500:
txt.append((p.x, p.y, e.dxf.text.strip()))
def near(p, q, tol=2.0):
return abs(p[0] - q[0]) < tol and abs(p[1] - q[1]) < tol
# union-find lines by shared endpoints
n = len(lines)
par = list(range(n))
def find(i):
while par[i] != i:
par[i] = par[par[i]]
i = par[i]
return i
pts = defaultdict(list)
for i, (a, b) in enumerate(lines):
pts[(round(a[0] / 3), round(a[1] / 3))].append(i)
pts[(round(b[0] / 3), round(b[1] / 3))].append(i)
for k, v in pts.items():
for j in v[1:]:
par[find(v[0])] = find(j)
comp = defaultdict(list)
for i in range(n):
comp[find(i)].append(i)
res = []
for (bx, by) in bub:
# label text inside bubble
lab = [t for t in txt if abs(t[0] - bx) < 260 and abs(t[1] - by) < 260 and t[2].startswith('C') and t[2][1:].isdigit()]
if not lab:
continue
name = lab[0][2]
ul = by - 252
cand = [i for i, (a, b) in enumerate(lines) if abs(a[1] - ul) < 4 and abs(b[1] - ul) < 4 and min(a[0], b[0]) < bx + 300 and max(a[0], b[0]) > bx - 300]
if not cand:
res.append((name, 'no underline'))
continue
got = set()
for i in cand:
for j in comp[find(i)]:
a, b = lines[j]
for p in (a, b):
for dd in dots:
if abs(p[0] - dd[0]) < 2 and 2 < abs(p[1] - dd[1]) < 9:
got.add(dd)
res.append((name, sorted(got)))
for name, g in sorted(res, key=lambda r: r[0]):
if isinstance(g, str):
print(name, g)
continue
rows = defaultdict(list)
for x, y in g:
rows[round(y, 1)].append(round(x - CX))
print(name, {y: sorted(v) for y, v in sorted(rows.items(), reverse=True)})
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""한 번에 시작: (1) Rhino MCP 서버 켜기 (2) 표식 클릭 자동 보기 켜기.
Rhino 명령줄: RunPythonScript → 이 파일 선택 (또는 Rhino 옵션 > 일반 > 시작 시 실행할 명령에 등록)
- MCP 서버(포트 9876)가 이미 떠 있으면 다시 켜지 않는다.
- 표식 자동 보기는 이 스크립트가 켬 상태로 만든다 (실행할 때마다 켜짐, 껐다 켜기 안 함).
"""
import socket
import Rhino
import scriptcontext as sc
CLIENT = u"D:\\★Pier 자료\\pier_v1\\Pier_T_rebar\\Code\\rhino_mcp\\rhino_mcp_client.py"
VIEWER = u"D:\\\u2605Pier \uc790\ub8cc\\\uc2ec\uc218\uc11d\ub2d8 \uac80\ud1a0 \ud30c\uc77c\\\uac80\ud1a0_\uc6b4\uace1\uad50P1\\\uc2a4\ud06c\ub9bd\ud2b8\\\ud45c\uc2dd_\ub0b4\uc6a9\ubcf4\uae30.py"
def port_open(port=9876):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(0.5)
try:
s.connect(("localhost", port))
return True
except Exception:
return False
finally:
s.close()
if port_open():
Rhino.RhinoApp.WriteLine(u"Rhino MCP 서버: 이미 켜져 있음 (9876)")
else:
Rhino.RhinoApp.RunScript(u'-_RunPythonScript "' + CLIENT + u'"', False)
Rhino.RhinoApp.WriteLine(u"Rhino MCP 서버: 켰음 (9876)")
sc.sticky["ungok_marker_launch"] = True
try:
Rhino.RhinoApp.RunScript(u'-_RunPythonScript "' + VIEWER + u'"', False)
finally:
sc.sticky["ungok_marker_launch"] = False
@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
"""운곡교 P1 코드 표식(코드표_*) 내용 보기.
사용법 (Rhino 명령줄에서 RunPythonScript → 이 파일 선택):
1) 표식(구슬·지시선·글자 중 아무거나)을 먼저 선택하고 실행 → 내용 창이 뜬다.
2) 아무것도 선택하지 않고 실행 → '선택하면 자동으로 창 띄우기' 켜기/끄기.
(켜 둔 동안 표식을 클릭하면 창이 뜬다. Rhino 를 끄면 해제된다.)
표식의 내용은 객체 속성 > 사용자 텍스트(Attribute User Text)에도 들어 있다.
"""
import Rhino
import scriptcontext as sc
import System
KEYS = [("code", u"코드"), ("gu", u"구분"), ("content", u"내용 (엑셀 비고코드설명)"), ("marks", u"관련 마크"),
("table_color", u"엑셀 대조표 색(판정)"), ("table_color_basis", u"색 근거"), ("detail", u"엑셀 ③ 대조·비고 내용"),
("where", u"출처")]
def build_text(obj):
a = obj.Attributes
if a.GetUserString("code") is None or a.GetUserString("code") == "":
return None
lines = []
for k, label in KEYS:
v = a.GetUserString(k)
if v:
if k == "detail":
lines.append(u"\n[" + label + u"]\n" + v)
else:
lines.append(u"[" + label + u"] " + v)
return u"\n".join(lines)
def show_for(objs):
seen = set()
texts = []
for o in objs:
t = build_text(o)
if t is None:
continue
code = o.Attributes.GetUserString("code")
if code in seen:
continue
seen.add(code)
texts.append(t)
if not texts:
return False
Rhino.UI.Dialogs.ShowTextDialog(u"\n\n" .join(texts) if len(texts) > 1 else texts[0], u"코드 표식 내용")
return True
def on_idle(sender, e):
Rhino.RhinoApp.Idle -= on_idle
pend = sc.sticky.get("ungok_marker_pending")
sc.sticky["ungok_marker_pending"] = None
if pend:
show_for(pend)
def on_select(sender, e):
if not sc.sticky.get("ungok_marker_auto"):
return
objs = [o for o in e.RhinoObjects if o.Attributes.GetUserString("code")]
if not objs:
return
sc.sticky["ungok_marker_pending"] = objs
Rhino.RhinoApp.Idle -= on_idle
Rhino.RhinoApp.Idle += on_idle
def main():
if sc.sticky.get("ungok_marker_launch"): # 라이노_시작.py 가 부른 경우: 무조건 '자동 보기 켬'
if not sc.sticky.get("ungok_marker_hooked"):
Rhino.RhinoDoc.SelectObjects += on_select
sc.sticky["ungok_marker_hooked"] = True
sc.sticky["ungok_marker_auto"] = True
Rhino.RhinoApp.WriteLine(u"표식 자동 보기: 켬 (표식을 클릭하면 창이 뜹니다)")
return
sel = [o for o in sc.doc.Objects.GetSelectedObjects(False, False)]
if sel and show_for(sel):
return
# 선택이 없거나 표식이 아니면 자동 모드 전환
if not sc.sticky.get("ungok_marker_hooked"):
Rhino.RhinoDoc.SelectObjects += on_select
sc.sticky["ungok_marker_hooked"] = True
sc.sticky["ungok_marker_auto"] = not sc.sticky.get("ungok_marker_auto", False)
Rhino.RhinoApp.WriteLine(u"표식 자동 보기: " + (u"켬 (표식을 클릭하면 창이 뜹니다)" if sc.sticky["ungok_marker_auto"] else u"끔"))
main()