- 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>
164 lines
8.4 KiB
Python
164 lines
8.4 KiB
Python
"""G2 gate checker, form "straight" (Ramp-A P1, 2026-10-13): straight main ring with a bottom coupler, lapped
|
|
half-ring hoops, X / Y cross ties; column on a drilled shaft (no footing).
|
|
|
|
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G2) come from the schedule (L, N, r)
|
|
and the D2 dimension chains (anchor EL 45.492: '2,000' below, 14@150 / 78@150 hoops, 7@300 / 39@300 ties) with
|
|
the user decisions K1-K3; generator params are not read.
|
|
Checks (C8): counts, original length (+coupler) vs schedule L, main radius / division / bottom / top, hoop and tie
|
|
level grids and radius, X / Y ties apart, containment in concrete (pile segments, column, coping), min cover (record).
|
|
"""
|
|
import json
|
|
import math
|
|
import os
|
|
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, TOL, G = X["pier_id"], X["tol_mm"], X["G2"]
|
|
SCHED_L, SCHED_N, DIA = G["schedule_L"], G["schedule_n"], G["dia"]
|
|
M, HP, TI = G["main"], G["hoops"], G["ties"]
|
|
A = G["chain_anchor"]
|
|
|
|
|
|
def grid(g):
|
|
return [A - g["below"] * g["pitch"] + k * g["pitch"] for k in range(SCHED_N[g["mark"]] // g.get("per_level", 1))]
|
|
|
|
|
|
HOOP_LEVELS, TIE_LEVELS = grid(HP), grid(TI)
|
|
CO = X["G1"]["column"]
|
|
R_COL, Z_CL0, Z_CL1 = CO["d"] / 2, CO["z0"], CO["z0"] + CO["length"]
|
|
results = []
|
|
|
|
|
|
def check(item, exp, act, tol=TOL):
|
|
results.append(dict(check="C8", 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="C8", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
|
|
|
|
|
def info(item, detail):
|
|
results.append(dict(check="C8", item=item, detail=detail, result="INFO"))
|
|
|
|
|
|
def ids(prefix):
|
|
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
|
|
|
|
|
|
def main():
|
|
cl = {m: [o for o in ids(f"{PID}-RC-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L}
|
|
pipes = {m: [o for o in ids(f"{PID}-R-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L}
|
|
occ = lambda o: o.Attributes.GetUserString("occurrence").rstrip("ab")
|
|
tie = TI["mark"]
|
|
for m in SCHED_L:
|
|
n_occ = len(cl[m]) if m == tie else len({occ(o) for o in cl[m]})
|
|
check(f"{m} bar count", SCHED_N[m], n_occ, tol=0)
|
|
flag(f"{m} pipes closed & paired with centrelines",
|
|
len(pipes[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pipes[m]), f"{len(pipes[m])}/{len(cl[m])}")
|
|
for m in SCHED_L:
|
|
per, curve = {}, {}
|
|
for o in cl[m]:
|
|
k = o.Attributes.GetUserString("occurrence") if m == tie else occ(o)
|
|
cp = float(o.Attributes.GetUserString("coupler_length") or 0)
|
|
per[k] = per.get(k, 0) + float(o.Attributes.GetUserString("design_length")) + cp
|
|
curve[k] = curve.get(k, 0) + o.Geometry.GetLength() + cp
|
|
if per:
|
|
check(f"{m} original length (+coupler) vs schedule L (worst of {len(per)})", 0,
|
|
max(abs(v - SCHED_L[m]) for v in per.values()))
|
|
dev = [v - SCHED_L[m] for v in curve.values()]
|
|
info(f"{m} curve length (+coupler) - L (record)", f"min {min(dev):.1f} / max {max(dev):.1f} mm")
|
|
# main bars: straight, chain bottom (anchor - 2,000), top = bottom + L - coupler (K1)
|
|
mm_ = M["mark"]
|
|
bottom = A - M["below_anchor"]
|
|
top = bottom + SCHED_L[mm_] - M["coupler"]
|
|
angs, worst_r, zb, zt = [], 0, [], []
|
|
for o in cl[mm_]:
|
|
c = o.Geometry
|
|
lo, hi = sorted((c.PointAtStart, c.PointAtEnd), key=lambda p: p.Z)
|
|
angs.append(math.degrees(math.atan2(hi.Y, hi.X)) % 360)
|
|
worst_r = max(worst_r, abs(math.hypot(lo.X, lo.Y) - M["radius"]), abs(math.hypot(hi.X, hi.Y) - M["radius"]))
|
|
zb.append(lo.Z)
|
|
zt.append(hi.Z)
|
|
angs.sort()
|
|
check(f"{mm_} radius (worst)", 0, worst_r, tol=0.5)
|
|
check(f"{mm_} angle vs {M['start_deg']} + i*360/{M['divisions']} (worst, deg)", 0,
|
|
max(abs(a - (M["start_deg"] + i * 360 / M["divisions"])) for i, a in enumerate(angs)), tol=0.01)
|
|
check(f"{mm_} bottom EL = chain {A / 1000:.3f} - {M['below_anchor']} (worst)", 0, max(abs(z - bottom) for z in zb))
|
|
check(f"{mm_} top EL = bottom + L - coupler {M['coupler']} (worst)", 0, max(abs(z - top) for z in zt))
|
|
# hoops: levels on the chain grid, two pieces per level, radius
|
|
hz = {}
|
|
for o in cl[HP["mark"]]:
|
|
bb = o.Geometry.GetBoundingBox(True)
|
|
hz.setdefault(round((bb.Max.Z + bb.Min.Z) / 2, 1), []).append(o)
|
|
zs = sorted(hz)
|
|
flag(f"{HP['mark']} levels {len(HOOP_LEVELS)} x 2 pieces", len(zs) == len(HOOP_LEVELS)
|
|
and all(len(v) == 2 for v in hz.values()), f"{len(zs)} levels")
|
|
check(f"{HP['mark']} levels vs chain grid {HP['pitch']} (worst)", 0,
|
|
max(abs(a - b) for a, b in zip(zs, HOOP_LEVELS)) if len(zs) == len(HOOP_LEVELS) else 1e9)
|
|
for o in cl[HP["mark"]][:4]:
|
|
c = o.Geometry
|
|
p = c.PointAt(c.Domain.Mid)
|
|
check(f"{HP['mark']} radius at mid point ({o.Attributes.GetUserString('occurrence')})", HP["radius"],
|
|
math.hypot(p.X, p.Y), tol=0.5)
|
|
# ties: X at the grid level, Y lifted (K3); levels = top of the straight part
|
|
by = lambda s: [o.Geometry for o in cl[tie] if o.Attributes.GetUserString("occurrence").endswith(s)]
|
|
xs, ys = by("X"), by("Y")
|
|
txm = sorted(round(g.GetBoundingBox(True).Max.Z, 1) for g in xs)
|
|
tym = sorted(round(g.GetBoundingBox(True).Max.Z, 1) for g in ys)
|
|
flag(f"{tie} X / Y per level, {len(TIE_LEVELS)} levels", len(txm) == len(tym) == len(TIE_LEVELS), f"X {len(txm)} Y {len(tym)}")
|
|
if len(txm) == len(TIE_LEVELS):
|
|
check(f"{tie} X levels vs chain grid {TI['pitch']} (worst)", 0, max(abs(a - b) for a, b in zip(txm, TIE_LEVELS)))
|
|
if len(tym) == len(TIE_LEVELS):
|
|
check(f"{tie} Y levels = X + {TI['y_lift']} (worst)", 0, max(abs(a - b - TI["y_lift"]) for a, b in zip(tym, TIE_LEVELS)))
|
|
xs.sort(key=lambda g: g.GetBoundingBox(True).Max.Z)
|
|
ys.sort(key=lambda g: g.GetBoundingBox(True).Max.Z)
|
|
gaps = []
|
|
for a, b in zip(xs, ys):
|
|
ok, pa, pb = a.ClosestPoints(b)
|
|
gaps.append(pa.DistanceTo(pb) if ok else 0.0)
|
|
gap = min(gaps) if gaps else 0.0
|
|
check(f"{tie} X / Y centrelines apart >= bar diameter {DIA[tie]} (min, capped)", DIA[tie], min(gap, DIA[tie]), tol=0.01)
|
|
info(f"{tie} X / Y centreline distance (min, record)", f"{gap:.2f} mm")
|
|
# containment and cover
|
|
solids = []
|
|
for o in all_objects(doc):
|
|
oid = o.Attributes.GetUserString("id") or ""
|
|
if oid in (f"{PID}-CL", f"{PID}-CP") or oid.startswith(f"{PID}-PL"):
|
|
solids.append(o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry))
|
|
union = rg.Brep.CreateBooleanUnion(solids, 0.01)
|
|
shell = union[0] if union and len(union) == 1 else None
|
|
info("concrete union for cover", f"{len(union) if union else 0} brep from {len(solids)} solids"
|
|
+ ("" if shell else " -> min cover = column side cover in the column zone (R - radial distance - bar radius)"))
|
|
min_cover = {}
|
|
for m in SCHED_L:
|
|
bad, cov = 0, 1e9
|
|
r = DIA[m] / 2
|
|
for o in cl[m]:
|
|
c = o.Geometry
|
|
for t in c.DivideByCount(max(4, int(c.GetLength() / 150)), True):
|
|
p = c.PointAt(t)
|
|
radial = rg.Vector3d(p.X, p.Y, 0)
|
|
radial.Unitize() if radial.Length > 1e-6 else None
|
|
for q in (p, p + radial * r, p - radial * r, 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 shell:
|
|
d = p.DistanceTo(shell.ClosestPoint(p)) if shell.IsPointInside(p, 0.01, False) else -1
|
|
cov = min(cov, d - r)
|
|
elif Z_CL0 <= p.Z <= Z_CL1: # no union (coplanar contact faces): side cover in the column zone
|
|
cov = min(cov, R_COL - math.hypot(p.X, p.Y) - r)
|
|
min_cover[m] = round(cov, 1)
|
|
check(f"{m} sample points outside concrete", 0, bad, tol=0)
|
|
info("min cover to concrete surface per mark (record)", json.dumps(min_cover))
|
|
write_result(RESULT_PATH, results, dict(min_cover=min_cover))
|
|
|
|
|
|
main()
|