- 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>
159 lines
7.9 KiB
Python
159 lines
7.9 KiB
Python
"""G3 gate checker, form "mats" (Ulsan P1, 2026-10-13): footing rebar groups, perimeter hoops, verticals.
|
|
|
|
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G3) from the schedule and the
|
|
position chains of the rebar drawing (layer heights are drawn values, marked [derived]); params are not read.
|
|
expected.G3: schedule {mark: [L, n]}, dia {mark: d}, rows {name: {first, start, pitch, n, last}},
|
|
layers {mark: {axis, z, row | pos, subset}} (subset "on"/"off": positions on / off the `of` row)
|
|
hoops {mark, half, chain {start, groups}}, verticals {mark, grid_x row, grid_y row, parity, z0, a}
|
|
Optional (Ulsan P2, 2026-10-15; absent -> unchanged): G1.footing.rot_deg (bars are checked after turning their centrelines back by the
|
|
foundation rotation), hoops.half_x / half_y, verticals {exclude {x, y} (grid points with |x| <= x and |y| <= y are absent), legs {bottom, top}}.
|
|
Checks (C9): counts, original length, layer z, positions, hoop levels / size, vertical grid, containment in the
|
|
footing, minimum cover (record), rebar colours by diameter.
|
|
"""
|
|
import json
|
|
import math
|
|
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, chain, load_expected, write_result # noqa: E402
|
|
|
|
doc = __rhino_doc__
|
|
X = load_expected(EXPECTED_PATH)
|
|
PID, G = X["pier_id"], X["G3"]
|
|
FB = X["G1"]["footing"]["z0"]
|
|
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
|
|
DIA = G["dia"]
|
|
PALETTE = {int(k): tuple(v) for k, v in X["rebar_palette"].items()}
|
|
ROT = X["G1"]["footing"].get("rot_deg", 0.0)
|
|
results = []
|
|
|
|
|
|
def bbx(o):
|
|
"""Bounding box of the bar centreline in the foundation's own axes."""
|
|
g = o.Geometry
|
|
if ROT:
|
|
g = g.DuplicateCurve()
|
|
g.Transform(rg.Transform.Rotation(-math.radians(ROT), rg.Vector3d.ZAxis, rg.Point3d.Origin))
|
|
return g.GetBoundingBox(True)
|
|
|
|
|
|
def check(item, exp, act, tol=1.0):
|
|
results.append(dict(check="C9", 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="C9", item=item, detail=detail, result="PASS" if ok else "FAIL"))
|
|
|
|
|
|
def info(item, detail):
|
|
results.append(dict(check="C9", item=item, detail=detail, result="INFO"))
|
|
|
|
|
|
def row(spec):
|
|
return sorted(list(spec.get("first", [])) + [spec["start"] + spec["pitch"] * i for i in range(spec["n"])]
|
|
+ list(spec.get("last", [])))
|
|
|
|
|
|
def positions(lay):
|
|
if "pos" in lay:
|
|
return sorted(lay["pos"])
|
|
base = row(G["rows"][lay["row"]])
|
|
if "subset" in lay:
|
|
ref = set(row(G["rows"][lay["of"]]))
|
|
base = [v for v in base if (v in ref) == (lay["subset"] == "on")]
|
|
return base
|
|
|
|
|
|
def objs(prefix, mark):
|
|
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)
|
|
and o.Attributes.GetUserString("mark") == mark]
|
|
|
|
|
|
def main():
|
|
cl = {m: objs(f"{PID}-RC-", m) for m in SCHED}
|
|
pp = {m: objs(f"{PID}-R-", 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 cl[m]:
|
|
check(f"{m} original length vs L (worst)", 0,
|
|
max(abs(float(o.Attributes.GetUserString("design_length")) - L) for o in cl[m]))
|
|
dev = [o.Geometry.GetLength() - L for o in cl[m]]
|
|
info(f"{m} curve length - L (bend fillets / laps)", f"min {min(dev):.1f} / max {max(dev):.1f}")
|
|
for m, lay in G["layers"].items():
|
|
if not cl[m]:
|
|
continue
|
|
# horizontal part: the lowest (legs up / none) or highest (legs down) straight segment -> bbox mid level
|
|
zs, ps = [], []
|
|
for o in cl[m]:
|
|
bb = bbx(o)
|
|
leg_down = lay.get("leg", 0) < 0
|
|
zs.append((bb.Max.Z if leg_down else bb.Min.Z) - FB)
|
|
ps.append(round((bb.Min.X + bb.Max.X) / 2 if lay["axis"] == "Y" else (bb.Min.Y + bb.Max.Y) / 2, 1))
|
|
check(f"{m} layer z (worst) [derived]", 0, max(abs(z - lay["z"]) for z in zs))
|
|
exp = positions(lay)
|
|
got = sorted(ps)
|
|
ok = len(got) == len(exp)
|
|
check(f"{m} positions vs chain (worst, n={len(got)}/{len(exp)})", 0,
|
|
max(abs(a - b) for a, b in zip(got, exp)) if ok else 1e9)
|
|
hp = G["hoops"]
|
|
lv = chain(hp["chain"], 0)
|
|
hz = sorted(round(bbx(o).Min.Z - FB, 1) for o in cl[hp["mark"]])
|
|
check(f"{hp['mark']} levels vs chain (worst)", 0, max(abs(a - b) for a, b in zip(hz, lv)) if len(hz) == len(lv) else 1e9)
|
|
if cl[hp["mark"]]:
|
|
bb = bbx(cl[hp["mark"]][0])
|
|
check(f"{hp['mark']} centreline size X [derived]", 2 * hp.get("half_x", hp.get("half")), bb.Max.X - bb.Min.X)
|
|
check(f"{hp['mark']} centreline size Y [derived]", 2 * hp.get("half_y", hp.get("half")), bb.Max.Y - bb.Min.Y)
|
|
vt = G["verticals"]
|
|
gx, gy = row(G["rows"][vt["grid_x"]]), row(G["rows"][vt["grid_y"]])
|
|
pts = {(round(bbx(o).Min.X), round(bbx(o).Min.Y)) for o in cl[vt["mark"]]}
|
|
if "exclude" in vt: # Ulsan P2: grid minus the central rectangle
|
|
ex = vt["exclude"]
|
|
exp = {(round(x), round(y)) for x in gx for y in gy if not (abs(x) <= ex["x"] and abs(y) <= ex["y"])}
|
|
flag(f"{vt['mark']} positions = {len(gx)}x{len(gy)} grid minus |x|<={ex['x']} & |y|<={ex['y']} [추론]", pts == exp, f"{len(pts)} / {len(exp)}")
|
|
else:
|
|
exp = {(round(x), round(y)) for i, x in enumerate(gx) for j, y in enumerate(gy) if (i + j) % 2 == vt["parity"]}
|
|
flag(f"{vt['mark']} positions = checkerboard of {len(gx)}x{len(gy)} grid [추론]", pts == exp, f"{len(pts)} / {len(exp)}")
|
|
if cl[vt["mark"]]:
|
|
check(f"{vt['mark']} bottom z (worst)", 0,
|
|
max(abs(bbx(o).Min.Z - FB - vt["z0"]) for o in cl[vt["mark"]]))
|
|
bb = bbx(cl[vt["mark"]][0])
|
|
check(f"{vt['mark']} vertical extent a", vt["a"], bb.Max.Z - bb.Min.Z)
|
|
if "legs" in vt: # both legs run towards +Y from the same point: Y extent = the longer leg
|
|
check(f"{vt['mark']} leg extent along Y (longer leg)", max(vt["legs"].values()), bb.Max.Y - bb.Min.Y)
|
|
ft = [o for o in all_objects(doc) if o.Attributes.GetUserString("id") == f"{PID}-FT"][0].Geometry
|
|
cover = {}
|
|
for m in SCHED:
|
|
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 ft.IsPointInside(q, 0.01, False):
|
|
bad += 1
|
|
cov = min(cov, p.DistanceTo(ft.ClosestPoint(p)) - r)
|
|
cover[m] = round(cov, 1)
|
|
check(f"{m} sample points outside footing", 0, bad, tol=0)
|
|
info("min cover to footing surface per mark (record; top face under the column counted)", json.dumps(cover))
|
|
wrong = 0
|
|
for o in all_objects(doc):
|
|
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.get(d):
|
|
wrong += 1
|
|
check("rebar pipe colour by diameter mismatches (all rebar)", 0, wrong, tol=0)
|
|
write_result(RESULT_PATH, results, dict(cover=cover))
|
|
|
|
|
|
main()
|