- 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
10 KiB
Python
208 lines
10 KiB
Python
"""G4 gate checker, form "polylines" (Ulsan P1, 2026-10-13): coping rebar C1~C15 and bearing-seat S1~S4.
|
|
|
|
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4) are the schedule (L, n, ranges)
|
|
and drawn coordinates of the rebar drawing (front / side views, sections, seat details); params are not read.
|
|
Checks (C10): counts, fixed-length marks vs L, variable dimension ranges vs schedule (record mean length),
|
|
top / bottom layer level, y rows and x extents, stirrup positions / pairs / heights, side-bar and hoop levels
|
|
and ends, C14 positions, seat bars, 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 row(spec):
|
|
return [spec["start"] + spec["pitch"] * i for i in range(spec["n"])]
|
|
|
|
|
|
def bb(o):
|
|
return o.Geometry.GetBoundingBox(True)
|
|
|
|
|
|
def worst(pairs):
|
|
return max((abs(a - b) for a, b in pairs), default=1e9)
|
|
|
|
|
|
def match(got, exp, tol=1.0):
|
|
"""Every expected tuple has one got tuple within tol (and same count)."""
|
|
got = list(got)
|
|
if len(got) != len(exp):
|
|
return False
|
|
for e in exp:
|
|
k = next((i for i, g in enumerate(got) if all(abs(a - b) <= tol for a, b in zip(g, e))), None)
|
|
if k is None:
|
|
return False
|
|
got.pop(k)
|
|
return True
|
|
|
|
|
|
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)
|
|
dl = {m: [float(o.Attributes.GetUserString("design_length")) for o in cl[m]] for m in SCHED}
|
|
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])}")
|
|
if not dl[m]:
|
|
continue
|
|
if m in G["fixed_length"]:
|
|
check(f"{m} original length vs L (worst)", 0, max(abs(v - L) for v in dl[m]))
|
|
else:
|
|
info(f"{m} mean original length vs L (variable mark, record)",
|
|
f"mean {statistics.mean(dl[m]):.1f} / L {L} (diff {statistics.mean(dl[m]) - L:+.1f}); min {min(dl[m]):.0f} max {max(dl[m]):.0f}")
|
|
# top / bottom layers
|
|
rows = {k: row(v) for k, v in G["rows"].items()}
|
|
cy_abs = G["corner"]["y_abs"]
|
|
clipped_x = set(G["corner"]["x_clipped"])
|
|
for m, lay in G["layers"].items():
|
|
if not cl[m]:
|
|
continue
|
|
ys = rows[lay["y"]]
|
|
if lay["parity"] is not None:
|
|
ys = [y for i, y in enumerate(ys) if i % 2 == lay["parity"]]
|
|
got = sorted(round((bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[m])
|
|
check(f"{m} y rows vs chain (worst, n={len(got)}/{len(ys)})", 0, worst(zip(got, ys)) if len(got) == len(ys) else 1e9)
|
|
if "z" in lay:
|
|
check(f"{m} level (top straight, worst)", 0, max(abs(bb(o).Max.Z - lay["z"]) for o in cl[m]))
|
|
else:
|
|
check(f"{m} bottom level (worst)", 0, max(abs(bb(o).Min.Z - lay["z_min"]) for o in cl[m]))
|
|
if m in G["x_extent"]:
|
|
x0, x1 = G["x_extent"][m]
|
|
inner = [o for o in cl[m] if abs((bb(o).Min.Y + bb(o).Max.Y) / 2) <= cy_abs]
|
|
check(f"{m} drawn x ends, |y| <= {cy_abs} (worst; corner fillets)", 0,
|
|
max(max(abs(bb(o).Min.X - x0), abs(bb(o).Max.X - x1)) for o in inner), tol=12)
|
|
info(f"{m} bars shortened at the R500 plan corners [추론]",
|
|
f"{sum(1 for o in cl[m] if o not in inner and bb(o).Max.X - bb(o).Min.X < x1 - x0 - 30)}"
|
|
f" of {len(cl[m]) - len(inner)} outer bars")
|
|
# stirrups
|
|
st = G["stirrups"]
|
|
for m, xs in st["x"].items():
|
|
got = {}
|
|
for o in cl[m]:
|
|
b = bb(o)
|
|
got.setdefault(round((b.Min.X + b.Max.X) / 2), []).append(o)
|
|
flag(f"{m} stirrup positions = drawn x ({len(xs)}) x 3", sorted(got) == sorted(round(x) for x in xs)
|
|
and all(len(v) == 3 for v in got.values()), f"{len(got)} positions")
|
|
pairs_ok, hw = True, 0
|
|
for x, os_ in got.items():
|
|
if x in clipped_x:
|
|
continue
|
|
pairs_ok &= match([(bb(o).Min.Y, bb(o).Max.Y) for o in os_], [tuple(p) for p in st["y_pairs"]])
|
|
exp_h = st["height"].get(f"{x:.0f}")
|
|
hw = max([hw] + [abs((bb(o).Max.Z - bb(o).Min.Z) - exp_h) for o in os_]) if exp_h else 1e9
|
|
flag(f"{m} legs at y pairs {st['y_pairs']} (A-A; corner positions {sorted(clipped_x)} excluded)", pairs_ok)
|
|
check(f"{m} centreline height vs drawn + 54 (worst)", 0, hw)
|
|
# variable ranges (schedule)
|
|
for m, (dim, lo, hi) in G["ranges"].items():
|
|
if not cl[m]:
|
|
continue
|
|
if dim == "B":
|
|
vals = [bb(o).Max.Z - bb(o).Min.Z for o in cl[m]]
|
|
check(f"{m} B min vs schedule '{lo}~{hi}'", lo, min(vals), tol=2)
|
|
check(f"{m} B max vs schedule '{lo}~{hi}'", hi, max(vals), tol=2)
|
|
else:
|
|
vals = []
|
|
for o in cl[m]:
|
|
b = bb(o)
|
|
a = b.Max.X - b.Min.X + (44 if m in ("C11", "C13") else (54 if m == "C10" else 44 - 790))
|
|
vals.append(a)
|
|
rng = f"model {min(vals):.0f}~{max(vals):.0f} / schedule {lo}~{hi}"
|
|
ok = abs(min(vals) - lo) <= 2 and abs(max(vals) - hi) <= 2
|
|
results.append(dict(check="C10", item=f"{m} A range vs schedule", detail=rng,
|
|
result="PASS" if ok else "INFO"))
|
|
# side bars and hoops per level
|
|
lv = G["levels"]
|
|
for key, yset in (("side", G["side"]["y"]), ("hoops", None)):
|
|
k0 = 0
|
|
for m, nlev in G[key]["marks"]:
|
|
exp = lv[k0:k0 + nlev]
|
|
k0 += nlev
|
|
got = []
|
|
for o in cl[m]:
|
|
b = bb(o)
|
|
c = o.Geometry.PointAt(o.Geometry.Domain.Mid)
|
|
got.append((round(b.Min.Z, 1), b.Min.X, b.Max.X, c.Y if yset else b.Max.Y - b.Min.Y))
|
|
want = [(zz, xl, xr, y) for zz, xl, xr in exp for y in (yset or [2 * G["hoops"]["y"]])]
|
|
flag(f"{m} levels / drawn ends / y ({len(want)})", match(got, want), f"{len(got)} bars")
|
|
# C14
|
|
c = G["c14"]
|
|
got = [((bb(o).Min.X + bb(o).Max.X) / 2, bb(o).Min.Z, bb(o).Min.Y, bb(o).Max.Y) for o in cl["C14"]]
|
|
n_corner = sum(1 for p in c["pts"] if round(abs(p[0]) + 19) in clipped_x)
|
|
full = [g for g in got if abs(g[2] - c["y"][0]) <= 1 and abs(g[3] - c["y"][1]) <= 1]
|
|
flag("C14 levels (drawn section marks)", match([(g[1],) for g in got], [(p[1],) for p in c["pts"]]), f"{len(got)}")
|
|
flag(f"C14 y span {c['y']} except {n_corner} corner bars", len(full) == len(got) - n_corner
|
|
and all(g[2] >= c["y"][0] - 1 and g[3] <= c["y"][1] + 1 for g in got), f"{len(full)} full")
|
|
# seat bars
|
|
se = G["seats"]
|
|
for no, s in se["bearings"].items():
|
|
cx, cy = s["centre"]
|
|
my, mx = s["marks"]
|
|
oy = [o for o in cl[my] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}")]
|
|
ox = [o for o in cl[mx] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}")]
|
|
flag(f"bearing {no} {my} x positions / y span", match([((bb(o).Min.X + bb(o).Max.X) / 2, bb(o).Min.Y, bb(o).Max.Y) for o in oy],
|
|
[(cx + d, cy - se["half"], cy + se["half"]) for d in se["offsets"]]), f"{len(oy)}")
|
|
flag(f"bearing {no} {mx} y positions / x span", match([((bb(o).Min.Y + bb(o).Max.Y) / 2, bb(o).Min.X, bb(o).Max.X) for o in ox],
|
|
[(cy + d, cx - se["half"], cx + se["half"]) for d in se["offsets"]]), f"{len(ox)}")
|
|
check(f"bearing {no} {my} level 50 below pedestal top (worst)", 0, max(abs(bb(o).Max.Z - s["z_top"]) for o in oy))
|
|
check(f"bearing {no} legs {se['leg']} (worst)", se["leg"], min(bb(o).Max.Z - bb(o).Min.Z for o in oy + ox))
|
|
# 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() / 150)), 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 coping surface per mark (record; seat bars measured to the coping only)", json.dumps(cover))
|
|
write_result(RESULT_PATH, results, dict(cover=cover))
|
|
|
|
|
|
main()
|