Files
Pier_v1/Code/울산P1판독/extract_s1.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 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>
2026-10-28 11:20:18 +09:00

199 lines
13 KiB
Python

"""S1 (shape) reading of RAG/교각배근도(울산P1).dwg -> app pier data/산외교울산P1 (params, expected, drawings).
Sheet: 일반도(1) (left part of the DWG, x -100000..-20000), drawn in mm.
Datums: front view column CL x -88384.7, footing bottom y -23989.8 (EL 42.763), column top / coping bottom
y -15389.8 (EL 67.863 = 42.763 + 2.500 + 22.600, coping drawn to scale above it); side view CL x -60315.3;
plan centre (-88384.7, -2498.5).
params = drawn entities (handles below) + ELEVATION / 솔플레이트 TABLE for bearing values.
expected = dimension / EL texts and tables (typed with their text), independent of params.
Scope of this stage: G1 (shape) and G5 (bearings, sole plates); rebar gates G2-G4 are out of scope.
"""
import json
import shutil
import sys
import time
from pathlib import Path
HERE = Path(__file__).resolve().parent
APP = HERE.parent / "교각모델링앱"
ROOT = HERE.parents[1]
PIER = APP / "data" / "산외교울산P1"
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 = -88384.7, -23989.8, -15389.8
PLAN = (-88384.7, -2498.5)
EL_FB, FT_T, COL_H = 42763, 2500, 22600 # 'EL. 42.763', '2.500', '22.600'
Z_CB = EL_FB + FT_T + COL_H # 67863
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)}
# rows keyed by bearing number; keep only rows that carry the table's own columns (a wide window can also
# catch numeric cells of a neighbouring table)
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():
template = json.loads((APP / "data" / "운곡교P1" / "params.json").read_text(encoding="utf-8"))
tmpl_e = json.loads((APP / "data" / "운곡교P1" / "expected.json").read_text(encoding="utf-8"))
if PIER.exists():
bk = PIER / "backup"
bk.mkdir(exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
for f in ("params.json", "expected.json"):
if (PIER / f).exists():
shutil.copy2(PIER / f, bk / f"{Path(f).stem}_before_S1_{stamp}.json")
(PIER / "drawings" / "dwg").mkdir(parents=True, exist_ok=True)
(PIER / "drawings" / "dxf").mkdir(parents=True, exist_ok=True)
src = ROOT / "RAG" / "교각배근도(울산P1).dwg"
shutil.copy2(src, PIER / "drawings" / "dwg" / src.name) # copy only; RAG original untouched
shutil.copy2(HERE / "dxf" / "교각배근도(울산P1).dxf", PIER / "drawings" / "dxf" / "교각배근도(울산P1).dxf")
audit = json.loads((HERE / "dxf" / "audit.json").read_text(encoding="utf-8"))
for a in audit:
a["label"] = "D1"
(PIER / "drawings" / "audit.json").write_text(json.dumps(audit, ensure_ascii=False, indent=1), encoding="utf-8")
shutil.copy2(HERE / "판독" / "texts.json", PIER / "drawings" / "texts.json")
texts = json.loads((HERE / "판독" / "texts.json").read_text(encoding="utf-8"))
el, sp = tables(texts)
# ---- params from drawn entities
ft, lc = line("FA9F"), line("FAA5") # footing / lean bottoms (front)
ft_side, lc_side = line("FC7E"), line("FC83")
col = (line("FA90")[0][0] - line("FA91")[0][0]) # column outline +-1250 (front)
tipL, tipR = line("FA67")[1], line("FA69")[0] # (-5000, 69563) / (+5000, 69563)
topL, topR = line("FA73")[0], line("FA6A")[0] # (-4700, 71193) / (+4700, 71063)
flat = line("FA68") # +-1250 at the coping bottom
zcb = z(flat[0][1])
profile = [[rx(flat[1][0]), zcb], [rx(flat[0][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])]]
P = {
"_schema": "pier-T/1", "pier_id": "P1",
"_meta": {"structure": "산외교 울산 P1 교각 (T형, 직선 경사 코핑, 직접기초)",
"status": "S1 형상 판독 2026-10-13 (Agent 초안, 사용자 확정 전)",
"units": "mm; Z = EL x 1000", "frame": "X 교축직각(정면도 좌→우), Y 교축(평면도 아래→위 = 전열→후열), 원점 = 교각 중심",
"sources": {"D1": "RAG/교각배근도(울산P1).dwg — 일반도(1) + 배근도(1)~(3)"}},
"scope": ["G1", "G5"],
"builders": {"G1": "concrete_tapered.py"},
"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": "일반도 정면 FAA5 / 측면 FC83 (7,700), 두께 100"},
"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(line("FA9E")[1][1] - line("FA9E")[0][1], 1),
"src": "일반도 정면 FA9F / 측면 FC7E (7,500), FA9E (2,500); 'EL. 42.763'; 말뚝 없음(직접기초)"},
"column": {"shape": "circular", "diameter": round(col, 1), "height": COL_H,
"src": "일반도 정면 FA90/FA91 (+-1,250), 단면 A-A 원 r 1,250 (FDAA); 높이 '22.600' (파단 작도)"},
"coping": {"form": "tapered", "profile_xz": profile, "width_y": round(line("FC61")[0][0] - line("FC61")[1][0], 1),
"plan_length": round(tipR[0] - tipL[0], 1), "top_length": round(topR[0] - topL[0], 1),
"z_tip": z(tipL[1]), "z_top_left": z(topL[1]), "z_top_right": z(topR[1]),
"plan_corner_radius": 500,
"src": {"profile": "일반도 정면 FA68 (하면 평탄), FA67/FA69 (하면 경사), FA73/FA6A (끝면 300 경사), FA66/FA62 (상면, 중심 130 단차)",
"width": "일반도 측면 FC61 (3,200)", "plan": "일반도 평면 FDBC~FDBF: 하단 10,000·상단 9,400 모두 모서리 R500 ('R=500')",
"not_modelled": "기둥·코핑 내부 배수관(단면 A-A '300'), 측면 하단 모서리 소형 모따기 [범위 밖]"}},
"materials": {"fck": {"lean": "15MPa", "footing": "27MPa", "column": "40MPa", "coping": "40MPa", "seat": "40MPa"},
"src": "일반도 주기 'fck = 27, 40 MPa', 기초 'fck = 27 MPa', '버림 CON'C fck = 15 MPa'; 기둥·코핑·대좌 40 [추론]"},
}
bearings = []
for no in sorted(el):
r = el[no]
x = -3250.0 if no in (1, 3) else 3250.0 # plan dims '1.750','6.500','1.750'
s1 = int(r["S1"])
y = -(1600 - s1) if no in (1, 2) else (1600 - s1) # S1 from the -Y edge (전열 1,2) / +Y edge (후열 3,4)
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(P["coping"]["z_top_left"] if x < 0 else P["coping"]["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 h, no in (("1025D", 2), ("10265", 1), ("1026D", 4), ("10271", 3)): # unused: drawn bearing CL crosses
pass
for hs, no in ((("1024D", "1024E"), 3), (("10245", "10246"), 4), (("10251", "10252"), 2), (("10259", "1025A"), 1)):
(x0, y0), (x1, y1) = line(hs[0])
(_, _), (x2, _) = line(hs[1])
drawn[no] = [round((x0 + x2) / 2 - PLAN[0], 1), round((y0 + y1) / 2 - PLAN[1], 1)]
P["bearing_seats"] = {
"_note": "pedestal = C3 x C6 box per bearing (h1); pad (h2) = frustum (A+200)x(B+200) -> (A+100)x(B+100)",
"pedestal": {"size_x": int(el[1]["C3"]), "size_y": int(el[1]["C6"])},
"pad_margin_bottom": 200, "pad_margin_top": 100,
"bearings": bearings, "drawn_pedestal_centres": drawn,
"src": {"table": "일반도 교량받침 ELEVATION TABLE (EL.'X', h1, h2, h3, h4, A, B, C1~C6, E, S1, S2)",
"pedestal": "교량받침 상세 C1+C2 = C3 (1,300), C4+C5 = C6 (1,300); 평면 작도 1,300 정사각",
"pad": "교량받침 상세 'h2','50','A','50','h2' -> 상면 A+100, 하면 A+100+2h2 = A+200 (h2 50)",
"position": "평면 '1.750','6.500','1.750' (X), S1 (Y); 작도 대좌 중심과 0.3 mm 이내 일치"},
}
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, "bearing_size": "AB", "bearing_shrink": 100,
"colours": {"bearing": [0, 0, 0], "sole_plate": [150, 150, 150]},
"sole_plate_src": "솔플레이트 상세 TABLE (S, L, W, T, T1, T2); L 교축 방향; S > 0 -> 두꺼운 쪽 +Y (T2 40)",
"src": "교량받침 = A x B (상세도: 받침 본체 A, 상판 L), h3 받침별(ELEVATION TABLE); 솔플레이트 두께 T = h4 31"}
P["display"] = dict(template["display"], hidden_ids=[], src="R8 팔레트(2026-10-07); 숨김 없음")
P["rebar_marks"] = template["rebar_marks"]
P["rebar_marks_src"] = "철근(G2~G4)은 이번 단계 범위 밖 — 운곡교 P1 템플릿 값(사용 안 함)"
# ---- expected from texts / tables
E = {"_schema": "pier-T-expected/1",
"_note": "독립 기대값: 일반도 치수 문자·EL 문자·표에서 옮긴 값. params와 별도.",
"pier_id": "P1", "tol_mm": 1.0, "checkers": {"G1": "g1_tapered.py"},
"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]}]},
"rebar_palette": tmpl_e["rebar_palette"], "planned_fail": []}
E["G1"] = {
"footing": {"dx": 7500, "dy": 7500, "z0": EL_FB, "thickness": 2500, "src": "'7.500' (정면·측면·기초 평면), 'EL. 42.763', '2.500'"},
"lean": {"margin": 100, "thickness": 100, "src": "'100','7.500','100'; 두께 '100'"},
"column": {"d": 2500, "height": 22600, "src": "단면 A-A '2.500', '22.600'"},
"coping": {"bottom_flat_half": 1250, "tip_half": 5000, "top_half": 4700, "soffit_rise": 1700, "width_y": 3200,
"z_top_left": 71193, "z_top_right": 71063, "plan_corner_radius": 500,
"src": "'1.250','1.250','3.750' x2; '10.000'; '300','2@4.700=9.400','300'; '1.700'; '3.200'; 'EL. 71.193','EL. 71.063'; 'R=500'"},
"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"},
}
E["G5"] = {
"el_x": {str(n): round(float(r["EL.'X'"]) * 1000) for n, r in el.items()},
"S": {str(r["no"]): r["S"] for r in rows}, "W": {str(r["no"]): r["W"] for r in rows},
"L": {str(r["no"]): r["L"] for r in rows}, "T": 31, "h3": {str(n): int(r["h3"]) for n, r in el.items()},
"bearing_size": {str(n): [int(r["A"]), int(r["B"])] for n, r in el.items()}, "bearing_shrink": 100,
"thick_end": "+Y", "colours": {"bearing": [0, 0, 0], "sole_plate": [150, 150, 150]},
"src": "ELEVATION TABLE (EL.'X', h3, A, B, h4=T); 솔플레이트 상세 TABLE (S, L, W, T1 22 / T2 40 -> 두꺼운 쪽 +Y)",
}
PIER.mkdir(parents=True, exist_ok=True)
if not (PIER / "pier.json").exists():
(PIER / "pier.json").write_text(json.dumps({"name": "산외교울산P1", "type": "T형", "created": time.strftime("%Y-%m-%d %H:%M:%S"),
"template": "판독(Code/울산P1판독)"}, ensure_ascii=False, indent=1), encoding="utf-8")
(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: P["coping"][k] for k in ("width_y", "plan_length", "top_length", "z_tip", "z_top_left", "z_top_right")})
print("footing", P["footing"]["size_x"], P["footing"]["size_y"], "lean", P["lean_concrete"]["size_x"], P["lean_concrete"]["size_y"], "column", P["column"]["diameter"])
print("bearings", [(b["no"], b["centre"], b["seat_z"], b["el_x"], b["h1"]) for b in bearings])
print("drawn pedestal centres", drawn)
if __name__ == "__main__":
main()