- 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>
208 lines
12 KiB
Python
208 lines
12 KiB
Python
"""G4 gate checker, Saejae-1 P1 coping + bearing-seat rebar (2026-10-15): C1~C20 and S1~S2 as centreline polylines (rectangular plan, no skew).
|
|
|
|
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4) come from the schedule (L, N, polyline length = a + legs),
|
|
the drawn levels / positions of the rebar drawing, the section dimension chains and the Okseong rules H1-H6 applied again; generator params
|
|
are not read. Checks (C10): counts / pipes / polyline length, top layers, C19 / C20, C7, stirrups (x, top, bottom, y), side loops and ties,
|
|
C18, seat bars per pedestal, containment in the coping + pedestals, minimum cover (record).
|
|
"""
|
|
import json
|
|
import os
|
|
import statistics
|
|
import sys
|
|
|
|
import Rhino.Geometry as rg
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from _common import all_objects, load_expected, write_result # noqa: E402
|
|
|
|
doc = __rhino_doc__
|
|
X = load_expected(EXPECTED_PATH)
|
|
PID, G = X["pier_id"], X["G4"]
|
|
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
|
results = []
|
|
|
|
|
|
def check(item, exp, act, tol=1.0):
|
|
results.append(dict(check="C10", item=item, expected=round(exp, 3), actual=round(act, 3),
|
|
diff=round(act - exp, 3), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
|
|
|
|
|
|
def flag(item, ok, detail=""):
|
|
results.append(dict(check="C10", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
|
|
|
|
|
def info(item, detail):
|
|
results.append(dict(check="C10", item=item, detail=detail, result="INFO"))
|
|
|
|
|
|
def bb(o):
|
|
return o.Geometry.GetBoundingBox(True)
|
|
|
|
|
|
def mid(o):
|
|
b = bb(o)
|
|
return (b.Min.X + b.Max.X) / 2, (b.Min.Y + b.Max.Y) / 2
|
|
|
|
|
|
def worst(got, want):
|
|
got, want = sorted(got), sorted(want)
|
|
return max((abs(a - b) for a, b in zip(got, want)), default=1e9) if len(got) == len(want) else 1e9
|
|
|
|
|
|
def occ(o):
|
|
return o.Attributes.GetUserString("occurrence") or ""
|
|
|
|
|
|
def main():
|
|
cl = {m: [] for m in SCHED}
|
|
pp = {m: [] for m in SCHED}
|
|
for o in all_objects(doc):
|
|
oid = o.Attributes.GetUserString("id") or ""
|
|
m = o.Attributes.GetUserString("mark")
|
|
if m in SCHED and oid.startswith(f"{PID}-RC-"):
|
|
cl[m].append(o)
|
|
elif m in SCHED and oid.startswith(f"{PID}-R-"):
|
|
pp[m].append(o)
|
|
for m, (L, n) in SCHED.items():
|
|
check(f"{m} bar count vs schedule", n, len(cl[m]), tol=0)
|
|
flag(f"{m} pipes closed & paired", len(pp[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pp[m]), f"{len(pp[m])}/{len(cl[m])}")
|
|
dl = [float(o.Attributes.GetUserString("design_length")) for o in cl[m]]
|
|
if m in G["fixed_length"]:
|
|
check(f"{m} polyline length vs a + legs (worst)", 0, max(abs(v - G["fixed_length"][m]) for v in dl) if dl else 1e9)
|
|
elif dl:
|
|
info(f"{m} mean polyline length vs L (variable mark, record)",
|
|
f"mean {statistics.mean(dl):.1f} / L {L} (diff {statistics.mean(dl) - L:+.1f}); min {min(dl):.0f} max {max(dl):.0f}")
|
|
# top layers C1~C6
|
|
tp, y23 = G["top"], G["y23"]
|
|
for m in ("C1", "C2", "C3", "C4", "C5", "C6"):
|
|
s = tp[m]
|
|
want = [y for i, y in enumerate(y23) if i % 2 == s["parity"]]
|
|
check(f"{m} y rows vs chain (worst, n={len(cl[m])}/{len(want)})", 0, worst([mid(o)[1] for o in cl[m]], want))
|
|
check(f"{m} level of the straight part (worst)", 0, max(abs(bb(o).Max.Z - s["z"]) for o in cl[m]))
|
|
check(f"{m} leg {s['leg']} (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - s["leg"]) for o in cl[m]))
|
|
check(f"{m} x ends +-{tp['x_end']} (worst)", 0, max(max(abs(bb(o).Min.X + tp["x_end"]), abs(bb(o).Max.X - tp["x_end"])) for o in cl[m]))
|
|
# C19, C20
|
|
c19 = G["c19"]
|
|
check("C19 y rows vs chain (worst)", 0, worst([mid(o)[1] for o in cl["C19"]], y23))
|
|
check("C19 level (worst)", 0, max(abs(bb(o).Max.Z - c19["z"]) for o in cl["C19"]))
|
|
check("C19 x ends (worst)", 0, max(max(abs(bb(o).Min.X - c19["x"][0]), abs(bb(o).Max.X - c19["x"][1])) for o in cl["C19"]))
|
|
check("C19 legs 370 (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - G["c20_leg"]) for o in cl["C19"]))
|
|
for k, pc in enumerate(G["c20"]):
|
|
grp = [o for o in cl["C20"] if abs(bb(o).Max.Z - pc["z"]) < 0.5 and abs(bb(o).Min.X - pc["x"][0]) < 3]
|
|
flag(f"C20 piece {k + 1}: 23 bars at EL {pc['z']}, x {pc['x']}", len(grp) == 23, f"{len(grp)}")
|
|
if grp:
|
|
check(f"C20 piece {k + 1} x ends (worst)", 0, max(max(abs(bb(o).Min.X - pc["x"][0]), abs(bb(o).Max.X - pc["x"][1])) for o in grp))
|
|
check(f"C20 piece {k + 1} leg 370 (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - G["c20_leg"]) for o in grp))
|
|
check(f"C20 piece {k + 1} y rows (worst)", 0, worst([mid(o)[1] for o in grp], y23))
|
|
# C7
|
|
c7 = G["c7"]
|
|
check("C7 lowest level per bar vs drawn (worst)", 0, worst([bb(o).Min.Z for o in cl["C7"]], c7["flat_z"]))
|
|
flat = [o for o in cl["C7"] if abs(mid(o)[1]) <= 875]
|
|
check("C7 top end EL 31.8453 (flat bars, worst)", 0, max(abs(bb(o).Max.Z - c7["top_end_z"]) for o in flat))
|
|
check("C7 x ends +-5875 (flat bars, worst)", 0, max(max(abs(bb(o).Min.X + c7["x_end"]), abs(bb(o).Max.X - c7["x_end"])) for o in flat))
|
|
raised = [o for o in cl["C7"] if 875 < abs(mid(o)[1]) <= 1125]
|
|
check("C7 raised bars (y +-1125) top end (worst)", 0, max(abs(bb(o).Max.Z - c7["raised_top_z"]) for o in raised))
|
|
edge = [o for o in cl["C7"] if abs(mid(o)[1]) > 1125]
|
|
check("C7 side-face bars capped at the cap level (worst)", 0, max(abs(bb(o).Max.Z - c7["cap_z"]) for o in edge), tol=1.5)
|
|
flag("C7 y rows: 12 @250", sorted(round(mid(o)[1]) for o in cl["C7"]) == [-1375 + 250 * k for k in range(12)])
|
|
# stirrups
|
|
st = G["stirrups"]
|
|
|
|
def zone(x):
|
|
return "left" if x < st["zone_cut"][0] else ("centre" if x <= st["zone_cut"][1] else "right")
|
|
|
|
def top_of(x):
|
|
return st["step_z"][sum(1 for b in st["step_b"] if x >= b)] - st["top_below"]
|
|
|
|
def bottom_of(x):
|
|
if abs(x) <= st["flat_half"]:
|
|
return st["flat_bottom"]
|
|
return st["flat_bottom"] - st["cover_n"] + st["soffit_slope"] * (abs(x) - st["flat_half"]) + st["cover_n"] * (1 + st["soffit_slope"] ** 2) ** 0.5
|
|
|
|
for zn, (rect, pent) in st["marks"].items():
|
|
sx = [x for x in st["x"] if zone(x) == zn]
|
|
want_r = [x + o for x in sx for o in st["off"]["R"]]
|
|
want_p = [x + o for x in sx for o in st["off"]["P"]]
|
|
check(f"{rect} positions = stations + x offset ({len(want_r)}, worst)", 0, worst([mid(o)[0] for o in cl[rect]], want_r), tol=0.5)
|
|
check(f"{pent} positions = stations + x offset ({len(want_p)}, worst)", 0, worst([mid(o)[0] for o in cl[pent]], want_p), tol=0.5)
|
|
dt = db = 0.0
|
|
for o in cl[rect] + cl[pent]:
|
|
xb = mid(o)[0]
|
|
dt = max(dt, abs(bb(o).Max.Z - top_of(xb)))
|
|
db = max(db, abs(bb(o).Min.Z - bottom_of(xb)))
|
|
check(f"{rect}/{pent} top = step top - {st['top_below']:.0f} (worst)", 0, dt, tol=1.5)
|
|
check(f"{rect}/{pent} bottom = soffit + cover along the normal (worst, sloped part <= 4)", 0, db, tol=4.0)
|
|
okr = all(any(abs(bb(o).Min.Y - a) < 2 and abs(bb(o).Max.Y - b) < 2 for a, b in st["rect_y"]) for o in cl[rect])
|
|
flag(f"{rect} y legs {st['rect_y']} (B-B)", okr)
|
|
pin, pout = st["pent_in"], st["pent_out"]
|
|
okp = all((abs(bb(o).Min.Y + pin) < 2 and abs(bb(o).Max.Y - pout) < 2) or (abs(bb(o).Max.Y - pin) < 2 and abs(bb(o).Min.Y + pout) < 2) for o in cl[pent])
|
|
flag(f"{pent} y between {pin:.0f} and {pout:.0f} (B-B)", okp)
|
|
# side loops and ties
|
|
lp = G["loops"]
|
|
for key in ("upper", "lower"):
|
|
sec = lp[key]
|
|
mloop, mtie = sec["marks"]
|
|
gl = sorted((round(bb(o).Min.Z, 1), round(bb(o).Max.X - bb(o).Min.X, 1), round((bb(o).Max.Y - bb(o).Min.Y) / 2, 1)) for o in cl[mloop])
|
|
wl = sorted((z, round(2 * x, 1), y) for z, x, y in zip(sec["z"], sec["x"], sec["y"]))
|
|
flag(f"{mloop} loops: level / x length / half width ({len(wl)})", len(gl) == len(wl) and all(abs(a[0] - b[0]) <= 1 and abs(a[1] - b[1]) <= 1 and abs(a[2] - b[2]) <= 1 for a, b in zip(gl, wl)), f"{gl[:2]}")
|
|
check(f"{mloop}/{mtie} horizontal (worst dz)", 0, max((bb(o).Max.Z - bb(o).Min.Z) for o in cl[mloop] + cl[mtie]), tol=0.01)
|
|
gt = sorted((round(bb(o).Min.Z, 1), round(mid(o)[1], 1), round(bb(o).Max.X - bb(o).Min.X, 1)) for o in cl[mtie])
|
|
wt = sorted((z, y, round(2 * x, 1)) for z, x in zip(sec["z"], sec["x"]) for y in lp["tie_y"])
|
|
flag(f"{mtie} ties: level / y / x length ({len(wt)})", len(gt) == len(wt) and all(abs(a[0] - b[0]) <= 1 and abs(a[1] - b[1]) <= 1 and abs(a[2] - b[2]) <= 1 for a, b in zip(gt, wt)), f"{len(gt)}")
|
|
# C18
|
|
c18 = G["c18"]
|
|
got = {}
|
|
for o in cl["C18"]: # upper rows: legs DOWN (straight part highest); lower rows: legs UP (lowest)
|
|
z = bb(o).Max.Z if bb(o).Max.Z > 32100 else bb(o).Min.Z
|
|
got.setdefault(f"{z:.1f}", []).append(o)
|
|
flag("C18 levels and counts", {k: len(v) for k, v in got.items()} == c18["levels"], f"{ {k: len(v) for k, v in got.items()} }")
|
|
check("C18 widths per level (worst; fillets on the slanted legs <= 30)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - c18["width"][k]) for k, v in got.items() if k in c18["width"] for o in v), tol=30)
|
|
# seats
|
|
se = G["seats"]
|
|
ped = {}
|
|
for o in all_objects(doc):
|
|
oid = o.Attributes.GetUserString("id") or ""
|
|
if oid.startswith(f"{PID}-BS"):
|
|
ped[int(oid[len(PID) + 3:])] = o.Geometry.GetBoundingBox(True)
|
|
flag("5 pedestals found", sorted(ped) == [1, 2, 3, 4, 5], f"{sorted(ped)}")
|
|
for k, pb in sorted(ped.items()):
|
|
cx, cy = (pb.Min.X + pb.Max.X) / 2, (pb.Min.Y + pb.Max.Y) / 2
|
|
g1 = [o for o in cl["S1"] if occ(o).startswith(f"P{k}-")]
|
|
g2 = [o for o in cl["S2"] if occ(o).startswith(f"P{k}-")]
|
|
flag(f"pedestal {k}: S1 11 / S2 20", (len(g1), len(g2)) == (11, 20), f"{len(g1)} {len(g2)}")
|
|
zt = pb.Max.Z - se["top_below"]
|
|
check(f"pedestal {k} S1 level {se['top_below']} below the pedestal top (worst, plane slope <= 20)", 0, max(abs(bb(o).Max.Z - zt) for o in g1), tol=20)
|
|
check(f"pedestal {k} S1 legs {se['leg']} (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - se["leg"]) for o in g1 + g2))
|
|
check(f"pedestal {k} S2 level 13 under S1 (worst)", 0, max(abs(bb(o).Max.Z - (min(bb(p).Max.Z for p in g1) - 13)) for o in g2), tol=1.5)
|
|
check(f"pedestal {k} S1 x positions (worst)", 0, worst([mid(o)[0] - cx for o in g1], se["s1_t"]), tol=1.5)
|
|
check(f"pedestal {k} S1 length along Y (worst)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - se["s1_len"]) for o in g1), tol=1.5)
|
|
check(f"pedestal {k} S2 y positions (worst)", 0, worst([mid(o)[1] - cy for o in g2], se["s2_o"]), tol=1.5)
|
|
check(f"pedestal {k} S2 length along X (worst)", 0, max(abs((bb(o).Max.X - bb(o).Min.X) - se["s2_len"]) for o in g2), tol=1.5)
|
|
# containment and cover
|
|
solids = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CP"
|
|
or (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-BS")]
|
|
solids = [s if isinstance(s, rg.Brep) else rg.Brep.TryConvertBrep(s) for s in solids]
|
|
cp = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CP"][0]
|
|
info("concrete solids for containment", f"{len(solids)} (coping + pedestals)")
|
|
cover = {}
|
|
for m in SCHED:
|
|
bad, cov = 0, 1e9
|
|
r = G["dia"][m] / 2
|
|
for o in cl[m]:
|
|
cu = o.Geometry
|
|
for t in cu.DivideByCount(max(6, int(cu.GetLength() / 200)), True):
|
|
p = cu.PointAt(t)
|
|
for q in (p, p + rg.Vector3d(r, 0, 0), p - rg.Vector3d(r, 0, 0), p + rg.Vector3d(0, r, 0),
|
|
p - rg.Vector3d(0, r, 0), p + rg.Vector3d(0, 0, r), p - rg.Vector3d(0, 0, r)):
|
|
if not any(s.IsPointInside(q, 0.01, False) for s in solids):
|
|
bad += 1
|
|
if cp.IsPointInside(p, 0.01, False):
|
|
cov = min(cov, p.DistanceTo(cp.ClosestPoint(p)) - r)
|
|
cover[m] = round(cov, 1)
|
|
check(f"{m} sample points outside concrete", 0, bad, tol=0)
|
|
info("min cover to the coping surface per mark (record; seat bars measured to the coping only)", json.dumps(cover))
|
|
write_result(RESULT_PATH, results, dict(cover=cover))
|
|
|
|
|
|
main()
|