- 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>
187 lines
12 KiB
Python
187 lines
12 KiB
Python
"""S1 (shape) reading of RAG/교각배근도(울산P2).dwg -> app pier data/산외교울산P2 (params, expected), scope G1 + G5.
|
|
|
|
Same sheet layout as Ulsan P1 (일반도(1), drawn in mm); the P2 differences were found by comparing the text sets and the
|
|
entities (2026-10-15): footing 7,800 x 7,500 on 30 steel pipe piles, ROTATED 37.000 deg against the pier axes, column 27.300 m,
|
|
all levels shifted; coping outline, bearings (4) and plan positions follow the same rules as P1.
|
|
Datums (front view, drawn): column CL x -85062.7; footing bottom y -23099.7 = EL 38.916; coping bottom y -14499.7
|
|
(EL 38.916 + 2.500 + 27.300 = 68.716, coping drawn to scale above it). Plan centre (-85062.7, -1608.4) = column circle 1ECD9.
|
|
params = drawn entities (handles in src) + ELEVATION / 솔플레이트 TABLE.
|
|
expected = dimension / EL texts and tables typed with their text, independent of params.
|
|
Rotation: the foundation plan is drawn in the foundation's own axes; the pier CL (arrow) lies at +52.5 deg, so the pier X axis is at
|
|
-37 deg in the foundation frame and the foundation is rotated +37 deg (counter-clockwise, X right / Y up) in the pier frame.
|
|
The drawing note '구조물 기초는 37.000˚회전' + '회전이 고려된 길이 9.767 m / 9.391 m' (= 7.800/cos37, 7.500/cos37) confirms the angle.
|
|
"""
|
|
import json
|
|
import math
|
|
import shutil
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
APP = HERE.parent / "교각모델링앱"
|
|
PIER = APP / "data" / "산외교울산P2"
|
|
sys.path.insert(0, str(HERE))
|
|
sys.path.insert(0, str(APP))
|
|
import dxftools as T # noqa: E402
|
|
from engine import reader # noqa: E402
|
|
|
|
E1 = T.doc("D1").entitydb
|
|
FX, FB_Y, CB_Y = -85062.7, -23099.7, -14499.7
|
|
PLAN = (-85062.7, -1608.4)
|
|
EL_FB, FT_T, COL_H = 38916, 2500, 27300 # 'EL. 38.916', '2.500', '27.300'
|
|
Z_CB = EL_FB + FT_T + COL_H # 68716
|
|
ROT_DEG = 37.0 # '구조물 기초는 37.000˚회전'
|
|
|
|
|
|
def line(h):
|
|
e = E1[h]
|
|
return (e.dxf.start.x, e.dxf.start.y), (e.dxf.end.x, e.dxf.end.y)
|
|
|
|
|
|
def z(y):
|
|
return round(Z_CB + (y - CB_Y), 1)
|
|
|
|
|
|
def rx(x):
|
|
return round(x - FX, 1)
|
|
|
|
|
|
def tables(texts):
|
|
tabs = {t["key"]: t for t in reader.table_candidates(texts)}
|
|
el = {int(r["_key"]): r for _, r in reader.table_records(tabs["elevation"]["rows"], str.isdigit) if "EL.'X'" in r}
|
|
sp = {int(r["_key"]): r for _, r in reader.table_records(tabs["sole_plate"]["rows"], str.isdigit)
|
|
if "S(%)" in r and int(r["_key"]) in el}
|
|
return el, sp
|
|
|
|
|
|
def main():
|
|
p1 = json.loads((APP / "data" / "산외교울산P1" / "params.json").read_text(encoding="utf-8"))
|
|
e1 = json.loads((APP / "data" / "산외교울산P1" / "expected.json").read_text(encoding="utf-8"))
|
|
bk = PIER / "backup"
|
|
bk.mkdir(exist_ok=True)
|
|
stamp = time.strftime("%Y%m%d_%H%M%S")
|
|
for f in ("params.json", "expected.json"):
|
|
shutil.copy2(PIER / f, bk / f"{Path(f).stem}_before_S1_{stamp}.json")
|
|
texts = json.loads((PIER / "drawings" / "texts.json").read_text(encoding="utf-8"))
|
|
el, sp = tables(texts)
|
|
|
|
# ---- drawn entities (handles from the 2026-10-15 dump)
|
|
ft, lc = line("1EA54"), line("1EA5A") # footing / lean bottoms, front (7,800 / 8,000)
|
|
ft_side, lc_side = line("1EBE5"), line("1EBEA") # side (7,500 / 7,700)
|
|
thick = line("1EBE4") # 2,500
|
|
col = abs(line("1EA45")[0][0] - line("1EA46")[0][0]) # column outline +-1250
|
|
flat = line("1EA1F") # coping bottom flat +-1250
|
|
tipL, tipR = line("1EA1E")[1], line("1EA20")[0] # (-5000, 1700) / (+5000, 1700) above the flat
|
|
topL, topR = line("1EA2A")[0], line("1EA21")[0] # (-4700, EL 72.046) / (+4700, EL 71.916)
|
|
zcb = z(flat[0][1])
|
|
assert abs(zcb - Z_CB) < 0.5, zcb
|
|
profile = [[-1250.0, zcb], [1250.0, zcb], [rx(tipR[0]), z(tipR[1])], [rx(topR[0]), z(topR[1])], [0.0, z(topR[1])],
|
|
[0.0, z(topL[1])], [rx(topL[0]), z(topL[1])], [rx(tipL[0]), z(tipL[1])]]
|
|
width = abs(line("1EBC8")[0][0] - line("1EBC8")[1][0])
|
|
|
|
P = json.loads(json.dumps(p1)) # start from P1 (same form), then replace what differs
|
|
P["pier_id"] = "P2"
|
|
P["_meta"] = {"structure": "산외교 울산 P2 교각 (T형, 직선 경사 코핑, 강관말뚝 기초 37° 회전)",
|
|
"status": "S1 형상 판독 2026-10-15 (Agent 초안, 사용자 확정 전)", "units": "mm; Z = EL x 1000",
|
|
"frame": "X 교축직각(정면도 좌→우), Y 교축(평면도 아래→위 = 전열→후열), 원점 = 교각(기둥) 중심. 기초(버림·기초판·말뚝)는 이 축에서 +37° 회전(평면도 반시계)",
|
|
"sources": {"D1": "RAG/교각배근도(울산P2).dwg — 일반도(2) + 배근도"}}
|
|
P["scope"] = ["G1", "G5"]
|
|
P["builders"] = {"G1": "concrete_tapered.py"}
|
|
P["lean_concrete"] = {"size_x": round(lc[0][0] - lc[1][0], 1), "size_y": round(lc_side[0][0] - lc_side[1][0], 1),
|
|
"z_bottom": EL_FB - 100, "z_top": EL_FB, "src": "일반도 정면 1EA5A (8,000) / 측면 1EBEA (7,700), 두께 100 (기초 자체 좌표계)"}
|
|
P["footing"] = {"size_x": round(ft[0][0] - ft[1][0], 1), "size_y": round(ft_side[0][0] - ft_side[1][0], 1),
|
|
"z_bottom": EL_FB, "thickness": round(abs(thick[0][1] - thick[1][1]), 1), "rot_deg": ROT_DEG,
|
|
"src": "일반도 정면 1EA54 (7,800) / 측면 1EBE5 (7,500), 1EBE4 (2,500); 'EL. 38.916'; 기초 평면도 '구조물 기초는 37.000˚회전' "
|
|
"(회전 길이 9.767 m = 7.800/cos37, 9.391 m = 7.500/cos37). 회전 방향: 기초 평면 교각 CL(화살표 +Y)이 +52.5° → 기초가 +37° [추론]"}
|
|
P["piles"] = {"count_x": 6, "count_y": 5, "pitch_x": 1300.0, "pitch_y": 1550.0, "diameter": 508.0, "length": 7000.0,
|
|
"head_embedment": 100.0, "kind": "강관말뚝 Ø508.0x9T (속찬 원기둥, O5)",
|
|
"src": "기초 평면도 '5@1.300=6.500' · '4@1.550=6.200', 가장자리 '650' + 버림 '100'; '(Φ508.0X9T, L=7.0M)'; 말뚝 머리 = 기초 하면 + 100 (정면 말뚝 상단 y -22999.7 vs 기초 하면 -23099.7); 속찬 원기둥은 옥성교 O5 결정 재적용 [추론]"}
|
|
P["column"] = {"shape": "circular", "diameter": round(col, 1), "height": COL_H,
|
|
"src": "일반도 정면 1EA45/1EA46 (+-1,250), 단면 A-A '2.500'; 높이 '27.300' (파단 작도)"}
|
|
c = P["coping"]
|
|
c["profile_xz"] = profile
|
|
c["width_y"] = round(width, 1)
|
|
c["plan_length"] = round(tipR[0] - tipL[0], 1)
|
|
c["top_length"] = round(topR[0] - topL[0], 1)
|
|
c["z_tip"], c["z_top_left"], c["z_top_right"] = z(tipL[1]), z(topL[1]), z(topR[1])
|
|
c["src"] = dict(c["src"], profile="일반도 정면 1EA1F (하면 평탄), 1EA1E/1EA20 (하면 경사), 1EA2A/1EA21 (끝면 300 경사), 1EA1D/1EA1C (상면, 중심 130 단차)",
|
|
width="일반도 측면 1EBC8 (3,200)")
|
|
# ---- seats (same rules as P1)
|
|
bearings = []
|
|
for no in sorted(el):
|
|
r = el[no]
|
|
x = -3250.0 if no in (1, 3) else 3250.0
|
|
s1 = int(r["S1"])
|
|
y = -(1600 - s1) if no in (1, 2) else (1600 - s1)
|
|
bearings.append({"no": no, "type": r["교량받침"], "row": "front" if no in (1, 2) else "rear",
|
|
"centre": [x, float(y)], "rotation_deg": 0.0,
|
|
"seat_z": float(c["z_top_left"] if x < 0 else c["z_top_right"]),
|
|
"el_x": round(float(r["EL.'X'"]) * 1000), "h1": int(r["h1"]), "h2": int(r["h2"]),
|
|
"A": int(r["A"]), "B": int(r["B"])})
|
|
drawn = {}
|
|
for no, hs in ((3, ("1F053", "1F054", "1F055")), (4, ("1F04B", "1F04C", "1F04D")), (1, ("1F063", "1F064", "1F065")),
|
|
(2, ("1F05B", "1F05C", "1F05D"))):
|
|
(x0, y0), (x1, y1) = line(hs[0])
|
|
(_, _), (x2, _) = line(hs[2])
|
|
ya, yb = line(hs[0])[0][1], line(hs[0])[1][1]
|
|
drawn[no] = [round((x0 + x2) / 2 - PLAN[0], 1), round((ya + yb) / 2 - PLAN[1], 1)]
|
|
bs = P["bearing_seats"]
|
|
bs["pedestal"] = {"size_x": int(el[1]["C3"]), "size_y": int(el[1]["C6"])}
|
|
bs["bearings"], bs["drawn_pedestal_centres"] = bearings, drawn
|
|
bs["src"] = dict(bs["src"], position="평면 '1.750','6.500','1.750' (X), S1 (Y); 작도 대좌(1F04B~1F066) 중심과 비교")
|
|
rows = []
|
|
for no in sorted(sp):
|
|
r = sp[no]
|
|
rows.append({"no": no, "S": float(r["S(%)"]), "L": int(r["L"]), "W": int(r["W"]), "T": int(r["T"]),
|
|
"T1": int(r["T1"]), "T2": int(r["T2"]), "h3": int(el[no]["h3"])})
|
|
P["sole_plates"]["table"] = rows
|
|
P["rebar_marks_src"] = "철근(G2~G4)은 이번 단계 범위 밖 — 울산 P1 값(사용 안 함)"
|
|
|
|
E = json.loads(json.dumps(e1))
|
|
E["pier_id"] = "P2"
|
|
E["_note"] = "독립 기대값: 일반도 치수 문자·EL 문자·표에서 옮긴 값. params와 별도."
|
|
E["checkers"] = {"G1": "g1_tapered.py"}
|
|
E["negative"] = {"G1": [{"id": "BS02", "d": [3, 0, 0]}, {"id": "BP03", "d": [0, 0, 2]}],
|
|
"G5": [{"id": "BB04", "d": [5, 0, 0]}, {"id": "SP03", "d": [0, 0, 5]}]}
|
|
E["planned_fail"] = []
|
|
for k in [k for k in E if k.startswith("G") and k not in ("G1", "G5")]:
|
|
del E[k]
|
|
G1 = E["G1"]
|
|
G1["footing"] = {"dx": 7800, "dy": 7500, "z0": EL_FB, "thickness": 2500, "rot_deg": ROT_DEG,
|
|
"src": "'7.800'(정면·기초 평면), '7.500'(측면·기초 평면), 'EL. 38.916', '2.500', '구조물 기초는 37.000˚회전' (회전 길이 9.767 = 7.800/cos37, 9.391 = 7.500/cos37)"}
|
|
G1["lean"] = {"margin": 100, "thickness": 100, "src": "'100','7.800','100' / '100','7.500','100'; 두께 '100'"}
|
|
G1["piles"] = {"nx": 6, "ny": 5, "pitch_x": 1300, "pitch_y": 1550, "edge_x": 650, "edge_y": 650, "d": 508, "length": 7000,
|
|
"head_embedment": 100, "src": "'5@1.300=6.500','650','100'; '4@1.550=6.200','650','100'; 'Φ508.0X9T, L=7.0M'; 머리 100(작도)"}
|
|
G1["column"] = {"d": 2500, "height": 27300, "src": "단면 A-A '2.500', '27.300'"}
|
|
G1["coping"] = dict(G1["coping"], z_top_left=72046, z_top_right=71916,
|
|
src="'1.250','1.250','3.750' x2; '10.000'; '300','2@4.700=9.400','300'; '1.700'; '3.200'; 'EL. 72.046','EL. 71.916'; 'R=500'")
|
|
G1["seats"] = {"table": {str(n): [round(float(r["EL.'X'"]) * 1000), int(r["h1"]), int(r["h2"]), int(r["A"]), int(r["B"])]
|
|
for n, r in el.items()},
|
|
"pos": {str(n): [-3250 if n in (1, 3) else 3250, -(1600 - int(r["S1"])) if n in (1, 2) else 1600 - int(r["S1"])]
|
|
for n, r in el.items()},
|
|
"pedestal": [int(el[1]["C3"]), int(el[1]["C6"])], "pad_margin": [200, 100],
|
|
"src": "ELEVATION TABLE; 평면 '1.750','6.500','1.750', S1; 상세 C3·C6, h2·50"}
|
|
G5 = E["G5"]
|
|
G5["el_x"] = {str(n): round(float(r["EL.'X'"]) * 1000) for n, r in el.items()}
|
|
G5["S"] = {str(r["no"]): r["S"] for r in rows}
|
|
G5["W"] = {str(r["no"]): r["W"] for r in rows}
|
|
G5["L"] = {str(r["no"]): r["L"] for r in rows}
|
|
G5["T"] = int(el[1]["h4"]) # sole plate thickness = h4 (P1: 31)
|
|
G5["h3"] = {str(n): int(r["h3"]) for n, r in el.items()}
|
|
G5["bearing_size"] = {str(n): [int(r["A"]), int(r["B"])] for n, r in el.items()}
|
|
(PIER / "params.json").write_text(json.dumps(P, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
(PIER / "expected.json").write_text(json.dumps(E, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
print("profile", profile)
|
|
print("coping", {k: c[k] for k in ("width_y", "plan_length", "top_length", "z_tip", "z_top_left", "z_top_right")})
|
|
print("lean", P["lean_concrete"]["size_x"], P["lean_concrete"]["size_y"], "footing", P["footing"]["size_x"], P["footing"]["size_y"],
|
|
P["footing"]["thickness"], "column", P["column"]["diameter"])
|
|
print("bearings", [(b["no"], b["centre"], b["seat_z"], b["el_x"], b["h1"], b["A"]) for b in bearings])
|
|
print("drawn pedestal centres", drawn)
|
|
print("sole plates", rows)
|
|
print("G5 T", G5["T"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|