- 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>
190 lines
10 KiB
Python
190 lines
10 KiB
Python
"""Verify coping rebar C1-C16, S1, S2 (check C10) inside Rhino 8.
|
|
|
|
Independent expected values typed from D3 schedule (counts, L, a/b ranges) and D1/D2 dimension chains.
|
|
Width decisions (K1: stirrups / C12-C13 drawn widths) make original-length checks of C9-C13 fail by design;
|
|
those are reported as FAIL (decision) and their varying dimension (b or a) is checked against the schedule
|
|
range instead. params.json is not read.
|
|
Globals: __rhino_doc__, RESULT_PATH; optional FINGERPRINT_REF / FINGERPRINT_OUT.
|
|
"""
|
|
import json
|
|
import math
|
|
|
|
import Rhino.Geometry as rg
|
|
from Rhino.DocObjects import ObjectColorSource, ObjectEnumeratorSettings
|
|
|
|
doc = __rhino_doc__
|
|
COUNT = {"C1": 12, "C2": 12, "C3": 12, "C4": 12, "C5": 12, "C6": 12, "C7": 12, "C8": 12, "C9": 60, "C10": 21,
|
|
"C11": 60, "C12": 18, "C13": 9, "C14": 41, "C15": 13, "C16": 13, "S1": 50, "S2": 105}
|
|
L = {"C1": 14490, "C2": 12081, "C3": 12106, "C4": 14490, "C5": 12081, "C6": 12106, "C7": 13994, "C8": 13994,
|
|
"C9": 8647, "C10": 10310, "C11": 8934, "C12": 20513, "C13": 29345, "C14": 3600, "C15": 2784, "C16": 5611,
|
|
"S1": 3800, "S2": 2500}
|
|
RANGE = {"C9": ("b_sched", 2956, 1376), "C11": ("b_sched", 2956, 1664), "C10": ("b_sched", 2998, 2998),
|
|
"C12": ("a_sched", 12228, 3151), "C13": ("a_sched", 12167, 12044)}
|
|
DIA = {m: (29 if m in ("C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8") else
|
|
25 if m in ("C9", "C10", "C11", "C12", "C13") else 19) for m in COUNT}
|
|
Y24 = sorted([100, 237] + [237 + 125 * k for k in range(1, 22)] + [3000]) # A-A chain
|
|
Y24 = [y - 1550 for y in Y24]
|
|
Y13 = [y - 1550 for y in [100, 237] + [237 + 250 * k for k in range(1, 11)] + [3000]] # B-B '10@250'
|
|
TIER = {"C1": 43311, "C4": 43311, "C2": 43211, "C5": 43211, "C3": 43111, "C6": 43111}
|
|
STIR_X = {"C10": sorted([0, 300, -300, 600, -600, 900, -900]),
|
|
"C11": [1150 + 250 * i for i in range(20)], "C9": sorted(-(1150 + 250 * i) for i in range(20))}
|
|
LOOP_Z = {"C12": [40761 + 250 * k for k in range(6)], "C13": [42261, 42511, 42761]}
|
|
PALETTE = {32: (214, 39, 40), 29: (255, 127, 14), 25: (188, 60, 160), 19: (44, 160, 44), 16: (31, 119, 180)}
|
|
PREFIXES = ("P1-R-C", "P1-RC-C", "P1-R-S", "P1-RC-S")
|
|
results = []
|
|
|
|
|
|
def ALL():
|
|
s = ObjectEnumeratorSettings()
|
|
s.HiddenObjects = True
|
|
s.LockedObjects = True
|
|
return list(doc.Objects.GetObjectList(s))
|
|
|
|
|
|
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 fingerprints():
|
|
fp = {}
|
|
for o in ALL():
|
|
oid = o.Attributes.GetUserString("id")
|
|
if not oid or oid.startswith(PREFIXES):
|
|
continue
|
|
g = o.Geometry
|
|
bb = g.GetBoundingBox(True)
|
|
# rebar pipes: face count (volume of ~1,000 pipes exceeded the 300 s tool timeout); centrelines: length
|
|
if isinstance(g, rg.Brep):
|
|
size = g.Faces.Count if oid.startswith("P1-R-") else rg.VolumeMassProperties.Compute(g).Volume / 1e6
|
|
else:
|
|
size = g.GetLength() if isinstance(g, rg.Curve) else 0
|
|
fp[oid] = [round(v, 3) for v in (bb.Min.X, bb.Min.Y, bb.Min.Z, bb.Max.X, bb.Max.Y, bb.Max.Z, size)]
|
|
return fp
|
|
|
|
|
|
def main():
|
|
if "FINGERPRINT_OUT" in globals():
|
|
fp = fingerprints()
|
|
json.dump(fp, open(FINGERPRINT_OUT, "w", encoding="utf-8"), indent=0)
|
|
print("fingerprints written", len(fp))
|
|
return
|
|
objs = ALL()
|
|
cl = {m: [o for o in objs if (o.Attributes.GetUserString("id") or "").startswith("P1-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("P1-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))
|
|
avg_conv = None
|
|
if key == "b_sched":
|
|
avg_conv = 2 * 1407 + (hi + lo) + 2 * 750 # schedule convention b = (max+min)/2
|
|
else:
|
|
avg_conv = (hi + lo) + 2 * 1407 + 4 * 580
|
|
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}")
|
|
# positions
|
|
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 (("C1", "C4"), ("C2", "C5"), ("C3", "C6"), ("C7", "C8")):
|
|
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 -1450",
|
|
[y for y, _ in ys] == Y24 and [m for _, m in ys] == [pair[i % 2] for i in range(24)])
|
|
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]:
|
|
per[round(o.Geometry.GetBoundingBox(True).Center.X)] = per.get(round(o.Geometry.GetBoundingBox(True).Center.X), 0) + 1
|
|
flag(f"{m} 3 stirrups per position", set(per.values()) == {3})
|
|
zb = [round(o.Geometry.GetBoundingBox(True).Min.Z) for o in cl["C10"]]
|
|
check("C10 stirrup bottom EL 40.511 (worst)", 0, max(abs(v - 40511) 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("C15 EL 43.598", 43598.4, max(o.Geometry.GetBoundingBox(True).Max.Z for o in cl["C15"]))
|
|
check("C16 EL 43.455", 43454.7, max(o.Geometry.GetBoundingBox(True).Max.Z for o in cl["C16"]))
|
|
# horizontal-segment height (bbox max picks up upward hooks: checker fix 2026-10-07)
|
|
c14z = sorted(round(o.Geometry.PointAt(o.Geometry.Domain.Mid).Z, 1) for o in cl["C14"])
|
|
exp14 = sorted([43088.5] * 11 + [43188.5] * 11 + [41011] * 3 + [42011] * 5 + [41511] * 4 + [42511] * 6 + [43442.4])
|
|
flag("C14 levels/count per level", c14z == exp14, "")
|
|
# S1/S2: per girder line 10/21, top EL = coping step top - 50 / -100 (D2 '50','50')
|
|
step = {1: 43411, 2: 43483, 3: 43555, 4: 43627, 5: 43698}
|
|
for m, n_per, dz in (("S1", 10, 50), ("S2", 21, 100)):
|
|
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] * 5)
|
|
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))
|
|
# containment in coping and cover
|
|
cp = [o for o in objs if o.Attributes.GetUserString("id") == "P1-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("P1-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)
|
|
if "FINGERPRINT_REF" in globals():
|
|
ref = json.load(open(FINGERPRINT_REF, encoding="utf-8"))
|
|
now = fingerprints()
|
|
worst = max(abs(a - b) for k in ref for a, b in zip(ref[k], now.get(k, [1e9] * 7)))
|
|
flag("existing objects (v0.1.04) unchanged", set(ref) == set(now) and worst <= 0.001,
|
|
f"{len(now)}/{len(ref)} max diff {worst}")
|
|
summ = dict(total=len([r for r in results if r["result"] != "INFO"]),
|
|
passed=sum(r["result"] == "PASS" for r in results),
|
|
failed=sum(r["result"] == "FAIL" for r in results), cover=cover)
|
|
json.dump(dict(summary=summ, results=results), open(RESULT_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
|
print(json.dumps(summ, ensure_ascii=False))
|
|
for r in results:
|
|
if r["result"] in ("FAIL", "INFO"):
|
|
print(r["result"], r["item"], r.get("detail", r.get("diff")))
|
|
|
|
|
|
main()
|