"""R3 (footing rebar) reading of the Ulsan P2 drawing (배근도(4)) -> data/산외교울산P2 params.footing_rebar, expected.G3; adds G3 to the pier scope (builder footing_rebar_mats.py, checker g3_mats.py). Same form as P1 (extract_r3.py there), P2 values (2026-10-15). Footing 7,800 (X, front view) x 7,500 (Y, side view) x 2,500, bars in the FOUNDATION's own axes (the foundation is rotated 37 deg, G1); the engine turns them with the footing. Front view footing x 5650..14078 (CX 9937), side view x 19802..27931 (CX 23940), y_bottom -39919. - position chains (front, X): top '100','300','28@250=7.000','300','100' -> -3800 | -3500..3500 @250 | 3800 (31 = B1); bottom '100','2@150','56@125=7.000','2@150','100' -> -3800,-3650 | -3500..3500 @125 | 3650,3800 (61 = B2 31 on top positions + B3 30) - side (Y): top '100','150','28@250=7.000','150','100' -> -3650 | -3500..3500 @250 | 3650 (31 = B4); bottom '100','150','56@125=7.000', '150','100' -> -3650 | -3500..3500 @125 | 3650 (59 = B5 31 on top positions + B6 28) - layer heights: drawn lines z 150 / 1,275 / 2,400 (same as P1) -> Y-direction bottom 127.5, X bottom 172.5, top Y 2422.5 / X 2377.5 [derived, P1 rule]; U bars B2/B5 legs up 2,250, lap J 1,160 (length only); L = A + 2B + J reproduces 12.960 / 13.260 - B8 perimeter hoops z 375..2,175 @300 (7; chain '225','6@300=1.800','225' + 150), centreline inset 77.5 (P1 rule) -> half 3822.5 x 3672.5; schedule L 33.330 vs 2A + 2B + 4J = 32,190 (P1 formula reproduces its own L) -> drawing / table conflict, recorded as planned FAIL - B9 (X, 7) / B10 (Y, 8) mid layer z 1,275, legs 100 down: positions are not dimensioned for these marks -> symmetric 1,000 pitch [추론] - B7 verticals (XI: A 2,285, legs 78 / 52, L 2,415): drawn in elevation at the 22 front x positions and 20 side y positions (CS-RBAR-EREC lines 2,250 long, z 150..2,400): chain '100','300','500','8@250','4@500','8@250','500','300','100' (front) / '100','400','500','7@250', '4@500','7@250','500','400','100' (side) without the centre point 0 -> 22 x 20 = 440 grid points; schedule 320 = 440 minus the points with |x| <= 2,000 and |y| <= 1,750 (120) [추론: the only rectangle that gives 320 besides its transpose]; z0 = 150 + (2,250 - 2,285) / 2 = 132.5 """ import json import shutil import time from pathlib import Path HERE = Path(__file__).resolve().parent APP = HERE.parent / "교각모델링앱" PIER = APP / "data" / "산외교울산P2" S = json.loads((HERE / "판독" / "schedule.json").read_text(encoding="utf-8")) ROWS = {r["mark"]: r for t in S.values() for r in t["rows"]} MARKS = [f"B{i}" for i in range(1, 11)] GRID_X = [-3500, -3000, -2750, -2500, -2250, -2000, -1750, -1500, -1250, -1000, -500, 500, 1000, 1250, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 3500] GRID_Y = [-3250, -2750, -2500, -2250, -2000, -1750, -1500, -1250, -1000, -500, 500, 1000, 1250, 1500, 1750, 2000, 2250, 2500, 2750, 3250] assert len(GRID_X) == 22 and len(GRID_Y) == 20 ROW_DEF = { "bottom_front": {"first": [-3800, -3650], "start": -3500, "pitch": 125, "n": 57, "last": [3650, 3800]}, "top_front": {"first": [-3800], "start": -3500, "pitch": 250, "n": 29, "last": [3800]}, "bottom_side": {"first": [-3650], "start": -3500, "pitch": 125, "n": 57, "last": [3650]}, "top_side": {"first": [-3650], "start": -3500, "pitch": 250, "n": 29, "last": [3650]}, "grid_x": {"first": GRID_X, "start": 0, "pitch": 1, "n": 0}, "grid_y": {"first": GRID_Y, "start": 0, "pitch": 1, "n": 0}, } MID_X = [-3500 + 1000 * i for i in range(8)] # B10 (8 bars along Y), positions along X [추론] MID_Y = [-3000 + 1000 * i for i in range(7)] # B9 (7 bars along X), positions along Y [추론] EXCLUDE = {"x": 2000, "y": 1750} def row(spec): return sorted(spec.get("first", []) + [spec["start"] + spec["pitch"] * i for i in range(spec["n"])] + spec.get("last", [])) def mm(v): return round(float(v) * 1000) if "." in v else int(v) def L(m): return round(float(ROWS[m]["_right"][0][1]) * 1000) # first value right of the J column = L def main(): P = json.loads((PIER / "params.json").read_text(encoding="utf-8")) E = json.loads((PIER / "expected.json").read_text(encoding="utf-8")) stamp = time.strftime("%Y%m%d_%H%M%S") for f in ("params.json", "expected.json"): shutil.copy2(PIER / f, PIER / "backup" / f"{Path(f).stem}_before_R3_{stamp}.json") bf, tf, bs_, ts = (row(ROW_DEF[k]) for k in ("bottom_front", "top_front", "bottom_side", "top_side")) assert len(bf) == 61 and len(tf) == 31 and len(bs_) == 59 and len(ts) == 31 on_f = [v for v in bf if v in tf] off_f = [v for v in bf if v not in tf] on_s = [v for v in bs_ if v in ts] off_s = [v for v in bs_ if v not in ts] leg_u, lap, hook = mm(ROWS["B2"]["B"]), mm(ROWS["B2"]["J"]), mm(ROWS["B9"]["B"]) a_y, a_x = mm(ROWS["B1"]["A"]), mm(ROWS["B4"]["A"]) # Y-direction bars 7,300, X-direction bars 7,600 src_f, src_s = "배근도(4) 정면도 기초", "배근도(4) 측면도 기초" groups = [ dict(mark="B1", axis="Y", z=2422.5, pos=tf, a=a_y, leg=0, src=f"{src_f} 상면 사슬 '28@250' (끝 3,800)"), dict(mark="B2", axis="Y", z=127.5, pos=on_f, a=a_y, leg=leg_u, lap=lap, src=f"{src_f} 하면 U형 (상면 위치), 이음 J {lap} 길이만"), dict(mark="B3", axis="Y", z=127.5, pos=off_f, a=a_y, leg=0, src=f"{src_f} 하면 '2@150','56@125' 중 B2 외"), dict(mark="B4", axis="X", z=2377.5, pos=ts, a=a_x, leg=0, src=f"{src_s} 상면 사슬 '28@250' (끝 3,650)"), dict(mark="B5", axis="X", z=172.5, pos=on_s, a=a_x, leg=leg_u, lap=lap, src=f"{src_s} 하면 U형 (측면 상면 위치) [추론]"), dict(mark="B6", axis="X", z=172.5, pos=off_s, a=a_x, leg=0, src=f"{src_s} 하면 B5 외"), dict(mark="B9", axis="X", z=1275, pos=MID_Y, a=a_x, leg=-hook, src=f"{src_s} 중앙: 위치 치수 없음, 1,000 간격 대칭 [추론]"), dict(mark="B10", axis="Y", z=1275, pos=MID_X, a=a_y, leg=-hook, src=f"{src_f} 중앙: 위치 치수 없음, 1,000 간격 대칭 [추론]; B9와 같은 높이"), ] hoop = dict(mark="B8", half_x=3822.5, half_y=3672.5, levels=[375 + 300 * i for i in range(7)], design_length=2 * mm(ROWS["B8"]["A"]) + 2 * mm(ROWS["B8"]["B"]) + 4 * mm(ROWS["B8"]["J"]), src="정면·측면 둘레 띠, z 375~2,175 @300; 중심선 안쪽 77.5 (울산P1 규칙); 길이 2A+2B+4J") pts = [[x, y] for x in GRID_X for y in GRID_Y if not (abs(x) <= EXCLUDE["x"] and abs(y) <= EXCLUDE["y"])] b_len, c_len = mm(ROWS["B7"]["B"]), mm(ROWS["B7"]["C"]) vert = dict(mark="B7", points=pts, z0=150 + (2250 - mm(ROWS["B7"]["A"])) / 2, a=mm(ROWS["B7"]["A"]), leg_bottom=b_len, leg_top=c_len, leg_axis="Y", src="입면 CS-RBAR-EREC 22 x 20 위치(중앙점 0 제외) 중 |x|<=2,000 & |y|<=1,750 제외 = 320 [추론]; 작도 z 150~2,400, A 중앙 배치; 다리 78 / 52 +Y [추론]") P["footing_rebar"] = {"form": "mats", "groups": groups, "hoops": hoop, "verticals": vert, "src": "배근도(4) 정면·측면 기초 배근 + 재료표 B1~B10 (기초 자체 축, 기초 회전은 footing.rot_deg)"} P["scope"] = [g for g in ("G1", "G2", "G3", "G4", "G5") if g in set(P["scope"]) | {"G3"}] P["builders"]["G3"] = "footing_rebar_mats.py" P["rebar_marks"] = {m: {"dia": r["dia"], "shape": r["shape"]} for m, r in ROWS.items()} assert "25" in P["display"]["rebar_palette"], "H25 colour missing" E["checkers"]["G3"] = "g3_mats.py" E["negative"]["G3"] = [{"mark": "B3", "d": [5, 0, 0]}, {"mark": "B7", "d": [0, 0, 5]}] E["G3"] = { "schedule": {m: [L(m), int(ROWS[m]["n"])] for m in MARKS}, "dia": {m: int(ROWS[m]["dia"][1:]) for m in MARKS}, "rows": ROW_DEF, "layers": { "B1": {"axis": "Y", "z": 2422.5, "row": "top_front"}, "B2": {"axis": "Y", "z": 127.5, "row": "bottom_front", "subset": "on", "of": "top_front"}, "B3": {"axis": "Y", "z": 127.5, "row": "bottom_front", "subset": "off", "of": "top_front"}, "B4": {"axis": "X", "z": 2377.5, "row": "top_side"}, "B5": {"axis": "X", "z": 172.5, "row": "bottom_side", "subset": "on", "of": "top_side"}, "B6": {"axis": "X", "z": 172.5, "row": "bottom_side", "subset": "off", "of": "top_side"}, "B9": {"axis": "X", "z": 1275, "pos": MID_Y, "leg": -hook}, "B10": {"axis": "Y", "z": 1275, "pos": MID_X, "leg": -hook}, }, "hoops": {"mark": "B8", "half_x": 3822.5, "half_y": 3672.5, "chain": {"start": [375], "groups": [[6, 300]]}}, "verticals": {"mark": "B7", "grid_x": "grid_x", "grid_y": "grid_y", "exclude": EXCLUDE, "z0": vert["z0"], "a": vert["a"], "legs": {"bottom": b_len, "top": c_len}}, "src": "재료표 L·수량; 정면·측면 위치 사슬(상면·하면·중앙); 층 높이는 작도값 [derived]", } E["planned_fail"] = [pf for pf in E.get("planned_fail", []) if pf.get("gate") != "G3"] + [ {"gate": "G3", "item": "B8 original length vs L (worst)", "reason": f"재료표 B8 L 33.330 vs 2A+2B+4J = {hoop['design_length']} (울산P1 B8은 같은 식이 L과 일치) — 도면·재료표 불일치 의심, 설계자 확인 대상(OPEN #32)", "approved": "미승인 — Agent 제안, 사용자 확인 대기"}] (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("counts", {g["mark"]: len(g["pos"]) for g in groups}, "B7", len(pts), "B8 len", hoop["design_length"], "B7 z0", vert["z0"]) print("schedule n", {m: int(ROWS[m]["n"]) for m in MARKS}) if __name__ == "__main__": main()