- 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>
186 lines
9.2 KiB
Python
186 lines
9.2 KiB
Python
"""G4 gate checker, Ramp-A P1 coping rebar (2026-10-14): C1~C16 and bearing-seat S1~S4 as centreline polylines.
|
|
|
|
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4, plus the G1 coping dimensions) come
|
|
from the D3 schedule (L, N), the D2 / D3 drawn positions and section dimensions, and the user decisions T1-T6;
|
|
generator params are not read.
|
|
Checks (C10): counts, fixed-length marks vs L (variable marks recorded), top layers (level, y rows, x ends),
|
|
stirrup positions per zone, stirrup top / bottom cover along the soffit, inner stirrup offset, side loops per level,
|
|
C6 bottom level, C15 / C16, seat bars per bearing, containment in the coping + pedestals, min cover (record).
|
|
"""
|
|
import json
|
|
import math
|
|
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, C, CO = X["pier_id"], X["G4"], X["G1"]["coping"], X["G1"]["column"]
|
|
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
|
ZB = CO["z0"] + CO["length"] # column top = coping bottom
|
|
ZT = ZB + C["soffit_rise"]
|
|
ZTL, ZTR = C["z_top_left"], C["z_top_right"]
|
|
R, HB, HT = C["soffit_radius"], C["column_face_half"], C["bottom_length"] / 2
|
|
results = []
|
|
|
|
|
|
def soffit_centre():
|
|
ax, az, bx, bz = -HB, ZB, -HT, ZT # R arc through (-HB, ZB) and (-HT, ZT), centre below the chord
|
|
mx, mz = (ax + bx) / 2, (az + bz) / 2
|
|
d = math.hypot(bx - ax, bz - az)
|
|
h = math.sqrt(R * R - d * d / 4)
|
|
ux, uz = (bz - az) / d, -(bx - ax) / d
|
|
return min(((mx + h * ux, mz + h * uz), (mx - h * ux, mz - h * uz)), key=lambda c: c[1])
|
|
|
|
|
|
SCX, SCZ = soffit_centre()
|
|
SCX = abs(SCX)
|
|
|
|
|
|
def soffit_z(x):
|
|
return ZB if abs(x) <= HB else SCZ + math.sqrt(R * R - (SCX - abs(x)) ** 2)
|
|
|
|
|
|
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(s):
|
|
return [s["start"] + s["pitch"] * i for i in range(s["n"])]
|
|
|
|
|
|
def bb(o):
|
|
return o.Geometry.GetBoundingBox(True)
|
|
|
|
|
|
def worst(pairs):
|
|
pairs = list(pairs)
|
|
return max((abs(a - b) for a, b in pairs), default=1e9)
|
|
|
|
|
|
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 not dl:
|
|
continue
|
|
if m in G["fixed_length"]:
|
|
check(f"{m} original length vs L (worst)", 0, max(abs(v - L) for v in dl))
|
|
else:
|
|
info(f"{m} mean original 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 (straight part = top of the bar)
|
|
for m, s in G["layers"].items():
|
|
got = sorted(round((bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[m])
|
|
want = row(s["y"])
|
|
check(f"{m} y rows (worst, {len(got)}/{len(want)})", 0, worst(zip(got, want)) if len(got) == len(want) else 1e9)
|
|
check(f"{m} level of the straight part (worst)", 0, max(abs(bb(o).Max.Z - s["z"]) for o in cl[m]))
|
|
if "x_top" in s:
|
|
x0, x1 = s["x_top"]
|
|
ends = []
|
|
for o in cl[m]:
|
|
c = o.Geometry
|
|
top_pts = [p for p in (c.PointAt(t) for t in c.DivideByLength(5, True)) if abs(p.Z - s["z"]) < 0.5]
|
|
ends.append(max(abs(min(p.X for p in top_pts) - x0), abs(max(p.X for p in top_pts) - x1)))
|
|
check(f"{m} straight part x ends vs drawn (worst; fillet tangent points)", 0, max(ends), tol=s.get("x_tol", 70))
|
|
# stirrups
|
|
st = G["stirrups"]
|
|
xs = row(st["x"])
|
|
for zone, (m_out, m_in, x0, x1) in st["zones"].items():
|
|
want = [x for x in xs if x0 <= x <= x1]
|
|
got_o = sorted(round(bb(o).Min.X, 1) for o in cl[m_out])
|
|
got_i = sorted(round(bb(o).Min.X, 1) for o in cl[m_in])
|
|
check(f"{m_out} positions = drawn x ({len(want)})", 0, worst(zip(got_o, want)) if len(got_o) == len(want) else 1e9)
|
|
check(f"{m_in} positions = drawn x + {st['inner_dx']} (2 per x)", 0,
|
|
worst(zip(got_i, sorted(x + st["inner_dx"] for x in want for _ in (0, 1)))) if len(got_i) == 2 * len(want) else 1e9)
|
|
for m in (m_out, m_in):
|
|
dt, dbm = [], []
|
|
for o in cl[m]:
|
|
b = bb(o)
|
|
x = b.Min.X - (st["inner_dx"] if m == m_in else 0)
|
|
t = ZTL if x <= 0 else ZTR
|
|
dt.append(abs((t - b.Max.Z) - st["top_cover"]))
|
|
dbm.append(abs((b.Min.Z - soffit_z(x)) - st["bottom_cover"] * (1 if abs(x) < HB else
|
|
# on the arc the 77.5 cover is radial: vertical gap = R' - R projected
|
|
(SCZ + math.sqrt((R + st["bottom_cover"]) ** 2 - (SCX - abs(x)) ** 2) - soffit_z(x)) / st["bottom_cover"])))
|
|
check(f"{m} top cover {st['top_cover']} (worst)", 0, max(dt))
|
|
check(f"{m} bottom cover {st['bottom_cover']} to the soffit (worst)", 0, max(dbm))
|
|
ys = [(bb(o).Min.Y, bb(o).Max.Y) for o in cl[m_out]]
|
|
check(f"{m_out} legs y {st['outer_y']} (worst)", 0, worst([(a, st["outer_y"][0]) for a, _ in ys] + [(b, st["outer_y"][1]) for _, b in ys]))
|
|
# side loops
|
|
lp = G["loops"]
|
|
for m, levels in lp["levels"].items():
|
|
zs = {}
|
|
for o in cl[m]:
|
|
zs.setdefault(round(bb(o).Min.Z, 1), []).append(o)
|
|
flag(f"{m} levels {levels} x {lp['per_level']}", sorted(zs) == sorted(levels)
|
|
and all(len(v) == lp["per_level"] for v in zs.values()), f"{sorted(zs)}")
|
|
flat = [abs(bb(o).Max.Z - bb(o).Min.Z) for o in cl[m]]
|
|
check(f"{m} loops horizontal (worst dz)", 0, max(flat) if flat else 1e9, tol=0.01)
|
|
# C6, C15, C16
|
|
c6 = G["c6"]
|
|
check("C6 bottom straight level (worst)", 0, max(abs(bb(o).Min.Z - c6["z_bottom"]) for o in cl["C6"]) if cl["C6"] else 1e9)
|
|
got = sorted(round(o.Geometry.PointAt(o.Geometry.Domain.Mid).Y, 1) for o in cl["C6"])
|
|
check("C6 y rows at mid-span (worst)", 0, worst(zip(got, c6["y"])) if len(got) == len(c6["y"]) else 1e9)
|
|
for m in ("C15",):
|
|
w = [bb(o).Max.Y - bb(o).Min.Y for o in cl[m]]
|
|
check(f"{m} width a (worst)", G["c15"]["a"], max(w, key=lambda v: abs(v - G["c15"]["a"])) if w else 0)
|
|
# seats
|
|
for no, want in G["seats"].items():
|
|
for m, n in want.items():
|
|
k = sum(1 for o in cl[m] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}-"))
|
|
check(f"bearing {no} {m} count", n, k, tol=0)
|
|
# 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]
|
|
col = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CL"]
|
|
info("concrete solids for containment", f"{len(solids)} (coping + pedestals); bars below the coping bottom checked in the column")
|
|
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 + col):
|
|
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()
|