- 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>
139 lines
7.3 KiB
Python
139 lines
7.3 KiB
Python
"""G4 gate checker: coping rebar C1-C16, S1, S2 (check C10) inside Rhino 8.
|
|
|
|
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values from EXPECTED_PATH section G4 (D3 counts, L,
|
|
a/b ranges; D1/D2 dimension chains). Width decisions (K1) make the original-length checks of the range marks
|
|
fail by design; they are reported as FAIL and listed in `planned_fail`. Generator params are not read.
|
|
Ported from Code/교각모델링/verify_coping_rebar.py (same checks and order).
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import Rhino.Geometry as rg
|
|
from Rhino.DocObjects import ObjectColorSource
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from _common import all_objects, load_expected, series, write_result, ychain # noqa: E402
|
|
|
|
doc = __rhino_doc__
|
|
X = load_expected(EXPECTED_PATH)
|
|
PID, G = X["pier_id"], X["G4"]
|
|
COUNT, L, DIA = G["count"], G["L"], G["dia"]
|
|
RANGE = {m: tuple(v) for m, v in G["range"].items()}
|
|
Y24 = ychain(G["y24_chain"], G["half_width"])
|
|
Y13 = ychain(G["y13_chain"], G["half_width"])
|
|
TIER = G["tier_z"]
|
|
STIR_X = {m: sorted(series(v)) for m, v in G["stirrup_x"].items()}
|
|
LOOP_Z = {m: series(v) for m, v in G["loop_z"].items()}
|
|
PALETTE = {int(k): tuple(v) for k, v in X["rebar_palette"].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 main():
|
|
objs = all_objects(doc)
|
|
cl = {m: [o for o in objs if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-RC-")
|
|
and o.Attributes.GetUserString("mark") == m] for m in COUNT}
|
|
pp = {m: [o for o in objs if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-R-")
|
|
and o.Attributes.GetUserString("mark") == m] for m in COUNT}
|
|
for m in COUNT:
|
|
check(f"{m} bar count", COUNT[m], 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]))
|
|
if not cl[m]:
|
|
continue
|
|
lens = [float(o.Attributes.GetUserString("design_length")) for o in cl[m]]
|
|
if m in RANGE:
|
|
key, hi, lo = RANGE[m]
|
|
vals = [float(o.Attributes.GetUserString(key)) for o in cl[m]]
|
|
check(f"{m} {key} max vs schedule", hi, max(vals))
|
|
check(f"{m} {key} min vs schedule", lo, min(vals))
|
|
cv = G["range_convention"][key]
|
|
fixed = cv["a"] if key == "b_sched" else cv["b"]
|
|
avg_conv = 2 * fixed + (hi + lo) + cv["c_times"] * cv["c"] # schedule convention: (max+min)/2
|
|
check(f"{m} schedule L reproduced by convention (max+min)/2", L[m], avg_conv)
|
|
ws = sorted({o.Attributes.GetUserString("w_model") for o in cl[m]})
|
|
results.append(dict(check="C10", item=f"{m} original length vs L (drawn widths {ws}, K1 decision)",
|
|
detail=f"model mean {sum(lens) / len(lens):.1f} vs L {L[m]}", result="FAIL"))
|
|
else:
|
|
check(f"{m} original length vs L (worst)", 0, max(abs(v - L[m]) for v in lens))
|
|
dev = [o.Geometry.GetLength() - float(o.Attributes.GetUserString("design_length")) for o in cl[m]]
|
|
info(f"{m} curve - original length", f"{min(dev):.1f} / {max(dev):.1f}")
|
|
for m, z in TIER.items():
|
|
zs = [o.Geometry.PointAt(o.Geometry.Domain.Mid).Z for o in cl[m]]
|
|
check(f"{m} tier EL (worst)", 0, max(abs(v - z) for v in zs))
|
|
for pair in G["pairs"]:
|
|
ys = sorted((round(o.Geometry.PointAt(o.Geometry.Domain.Mid).Y), m) for m in pair for o in cl[m])
|
|
flag(f"{'/'.join(pair)} Y = A-A chain, alternate from {pair[0]} at {Y24[0]:.0f}",
|
|
[y for y, _ in ys] == Y24 and [m for _, m in ys] == [pair[i % 2] for i in range(len(Y24))])
|
|
for m, xs in STIR_X.items():
|
|
got = sorted({round(o.Geometry.GetBoundingBox(True).Center.X) for o in cl[m]})
|
|
flag(f"{m} stirrup X positions", got == xs, f"{len(got)} positions")
|
|
per = {}
|
|
for o in cl[m]:
|
|
k = round(o.Geometry.GetBoundingBox(True).Center.X)
|
|
per[k] = per.get(k, 0) + 1
|
|
flag(f"{m} {G['stirrups_per_x']} stirrups per position", set(per.values()) == {G["stirrups_per_x"]})
|
|
zb = [round(o.Geometry.GetBoundingBox(True).Min.Z) for o in cl["C10"]]
|
|
check(f"C10 stirrup bottom EL {G['c10_bottom'] / 1000:.3f} (worst)", 0, max(abs(v - G["c10_bottom"]) for v in zb))
|
|
for m, zs in LOOP_Z.items():
|
|
got = sorted({round(o.Geometry.GetBoundingBox(True).Center.Z) for o in cl[m]})
|
|
flag(f"{m} loop levels", got == zs, str(got))
|
|
for m in ("C15", "C16"):
|
|
got = sorted(round(o.Geometry.GetBoundingBox(True).Center.Y) for o in cl[m])
|
|
flag(f"{m} Y = B-B chain", got == Y13, f"{len(got)}")
|
|
check(f"C15 EL {G['c15_top'] / 1000:.3f}", G["c15_top"], max(o.Geometry.GetBoundingBox(True).Max.Z for o in cl["C15"]))
|
|
check(f"C16 EL {G['c16_top'] / 1000:.3f}", G["c16_top"], max(o.Geometry.GetBoundingBox(True).Max.Z for o in cl["C16"]))
|
|
# horizontal-segment height (bbox max picks up upward hooks)
|
|
c14z = sorted(round(o.Geometry.PointAt(o.Geometry.Domain.Mid).Z, 1) for o in cl["C14"])
|
|
exp14 = sorted(z for z, n in G["c14_levels"] for _ in range(n))
|
|
flag("C14 levels/count per level", c14z == exp14, "")
|
|
step = {int(k): v for k, v in G["step_top"].items()}
|
|
for m, (n_per, dz) in G["s_per_girder"].items():
|
|
by = {}
|
|
for o in cl[m]:
|
|
g = int(o.Attributes.GetUserString("occurrence")[1])
|
|
by.setdefault(g, []).append(o.Geometry.GetBoundingBox(True).Max.Z)
|
|
flag(f"{m} per girder line = {n_per}", sorted(len(v) for v in by.values()) == [n_per] * len(step))
|
|
check(f"{m} EL = step top - {dz} (worst)", 0, max(abs(z - (step[g] - dz)) for g, v in by.items() for z in v))
|
|
cp = [o for o in objs if o.Attributes.GetUserString("id") == f"{PID}-CP"][0].Geometry
|
|
cover = {}
|
|
for m in COUNT:
|
|
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() / 200)), True):
|
|
p = c.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 cp.IsPointInside(q, 0.01, False):
|
|
bad += 1
|
|
cov = min(cov, p.DistanceTo(cp.ClosestPoint(p)) - r)
|
|
cover[m] = round(cov, 1)
|
|
check(f"{m} sample points outside coping", 0, bad, tol=0)
|
|
info("min cover to coping surface per mark (top face under pedestals counted)", json.dumps(cover))
|
|
wrong = 0
|
|
for o in objs:
|
|
if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-R-"):
|
|
d = int(o.Attributes.GetUserString("dia")[1:])
|
|
c = o.Attributes.ObjectColor
|
|
if o.Attributes.ColorSource != ObjectColorSource.ColorFromObject or (c.R, c.G, c.B) != PALETTE[d]:
|
|
wrong += 1
|
|
check("rebar colour mismatches (all rebar)", 0, wrong, tol=0)
|
|
write_result(RESULT_PATH, results, dict(cover=cover))
|
|
|
|
|
|
main()
|