Files
Pier_v1/Code/교각모델링앱/engine/rhino/checks/g4_polylines.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- 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>
2026-10-28 11:20:18 +09:00

208 lines
10 KiB
Python

"""G4 gate checker, form "polylines" (Ulsan P1, 2026-10-13): coping rebar C1~C15 and bearing-seat S1~S4.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4) are the schedule (L, n, ranges)
and drawn coordinates of the rebar drawing (front / side views, sections, seat details); params are not read.
Checks (C10): counts, fixed-length marks vs L, variable dimension ranges vs schedule (record mean length),
top / bottom layer level, y rows and x extents, stirrup positions / pairs / heights, side-bar and hoop levels
and ends, C14 positions, seat bars, containment in the coping + pedestals, minimum cover (record).
"""
import json
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 = X["pier_id"], X["G4"]
SCHED = {m: tuple(v) for m, v in G["schedule"].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 row(spec):
return [spec["start"] + spec["pitch"] * i for i in range(spec["n"])]
def bb(o):
return o.Geometry.GetBoundingBox(True)
def worst(pairs):
return max((abs(a - b) for a, b in pairs), default=1e9)
def match(got, exp, tol=1.0):
"""Every expected tuple has one got tuple within tol (and same count)."""
got = list(got)
if len(got) != len(exp):
return False
for e in exp:
k = next((i for i, g in enumerate(got) if all(abs(a - b) <= tol for a, b in zip(g, e))), None)
if k is None:
return False
got.pop(k)
return True
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)
dl = {m: [float(o.Attributes.GetUserString("design_length")) for o in cl[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 not dl[m]:
continue
if m in G["fixed_length"]:
check(f"{m} original length vs L (worst)", 0, max(abs(v - L) for v in dl[m]))
else:
info(f"{m} mean original length vs L (variable mark, record)",
f"mean {statistics.mean(dl[m]):.1f} / L {L} (diff {statistics.mean(dl[m]) - L:+.1f}); min {min(dl[m]):.0f} max {max(dl[m]):.0f}")
# top / bottom layers
rows = {k: row(v) for k, v in G["rows"].items()}
cy_abs = G["corner"]["y_abs"]
clipped_x = set(G["corner"]["x_clipped"])
for m, lay in G["layers"].items():
if not cl[m]:
continue
ys = rows[lay["y"]]
if lay["parity"] is not None:
ys = [y for i, y in enumerate(ys) if i % 2 == lay["parity"]]
got = sorted(round((bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[m])
check(f"{m} y rows vs chain (worst, n={len(got)}/{len(ys)})", 0, worst(zip(got, ys)) if len(got) == len(ys) else 1e9)
if "z" in lay:
check(f"{m} level (top straight, worst)", 0, max(abs(bb(o).Max.Z - lay["z"]) for o in cl[m]))
else:
check(f"{m} bottom level (worst)", 0, max(abs(bb(o).Min.Z - lay["z_min"]) for o in cl[m]))
if m in G["x_extent"]:
x0, x1 = G["x_extent"][m]
inner = [o for o in cl[m] if abs((bb(o).Min.Y + bb(o).Max.Y) / 2) <= cy_abs]
check(f"{m} drawn x ends, |y| <= {cy_abs} (worst; corner fillets)", 0,
max(max(abs(bb(o).Min.X - x0), abs(bb(o).Max.X - x1)) for o in inner), tol=12)
info(f"{m} bars shortened at the R500 plan corners [추론]",
f"{sum(1 for o in cl[m] if o not in inner and bb(o).Max.X - bb(o).Min.X < x1 - x0 - 30)}"
f" of {len(cl[m]) - len(inner)} outer bars")
# stirrups
st = G["stirrups"]
for m, xs in st["x"].items():
got = {}
for o in cl[m]:
b = bb(o)
got.setdefault(round((b.Min.X + b.Max.X) / 2), []).append(o)
flag(f"{m} stirrup positions = drawn x ({len(xs)}) x 3", sorted(got) == sorted(round(x) for x in xs)
and all(len(v) == 3 for v in got.values()), f"{len(got)} positions")
pairs_ok, hw = True, 0
for x, os_ in got.items():
if x in clipped_x:
continue
pairs_ok &= match([(bb(o).Min.Y, bb(o).Max.Y) for o in os_], [tuple(p) for p in st["y_pairs"]])
exp_h = st["height"].get(f"{x:.0f}")
hw = max([hw] + [abs((bb(o).Max.Z - bb(o).Min.Z) - exp_h) for o in os_]) if exp_h else 1e9
flag(f"{m} legs at y pairs {st['y_pairs']} (A-A; corner positions {sorted(clipped_x)} excluded)", pairs_ok)
check(f"{m} centreline height vs drawn + 54 (worst)", 0, hw)
# variable ranges (schedule)
for m, (dim, lo, hi) in G["ranges"].items():
if not cl[m]:
continue
if dim == "B":
vals = [bb(o).Max.Z - bb(o).Min.Z for o in cl[m]]
check(f"{m} B min vs schedule '{lo}~{hi}'", lo, min(vals), tol=2)
check(f"{m} B max vs schedule '{lo}~{hi}'", hi, max(vals), tol=2)
else:
vals = []
for o in cl[m]:
b = bb(o)
a = b.Max.X - b.Min.X + (44 if m in ("C11", "C13") else (54 if m == "C10" else 44 - 790))
vals.append(a)
rng = f"model {min(vals):.0f}~{max(vals):.0f} / schedule {lo}~{hi}"
ok = abs(min(vals) - lo) <= 2 and abs(max(vals) - hi) <= 2
results.append(dict(check="C10", item=f"{m} A range vs schedule", detail=rng,
result="PASS" if ok else "INFO"))
# side bars and hoops per level
lv = G["levels"]
for key, yset in (("side", G["side"]["y"]), ("hoops", None)):
k0 = 0
for m, nlev in G[key]["marks"]:
exp = lv[k0:k0 + nlev]
k0 += nlev
got = []
for o in cl[m]:
b = bb(o)
c = o.Geometry.PointAt(o.Geometry.Domain.Mid)
got.append((round(b.Min.Z, 1), b.Min.X, b.Max.X, c.Y if yset else b.Max.Y - b.Min.Y))
want = [(zz, xl, xr, y) for zz, xl, xr in exp for y in (yset or [2 * G["hoops"]["y"]])]
flag(f"{m} levels / drawn ends / y ({len(want)})", match(got, want), f"{len(got)} bars")
# C14
c = G["c14"]
got = [((bb(o).Min.X + bb(o).Max.X) / 2, bb(o).Min.Z, bb(o).Min.Y, bb(o).Max.Y) for o in cl["C14"]]
n_corner = sum(1 for p in c["pts"] if round(abs(p[0]) + 19) in clipped_x)
full = [g for g in got if abs(g[2] - c["y"][0]) <= 1 and abs(g[3] - c["y"][1]) <= 1]
flag("C14 levels (drawn section marks)", match([(g[1],) for g in got], [(p[1],) for p in c["pts"]]), f"{len(got)}")
flag(f"C14 y span {c['y']} except {n_corner} corner bars", len(full) == len(got) - n_corner
and all(g[2] >= c["y"][0] - 1 and g[3] <= c["y"][1] + 1 for g in got), f"{len(full)} full")
# seat bars
se = G["seats"]
for no, s in se["bearings"].items():
cx, cy = s["centre"]
my, mx = s["marks"]
oy = [o for o in cl[my] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}")]
ox = [o for o in cl[mx] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}")]
flag(f"bearing {no} {my} x positions / y span", match([((bb(o).Min.X + bb(o).Max.X) / 2, bb(o).Min.Y, bb(o).Max.Y) for o in oy],
[(cx + d, cy - se["half"], cy + se["half"]) for d in se["offsets"]]), f"{len(oy)}")
flag(f"bearing {no} {mx} y positions / x span", match([((bb(o).Min.Y + bb(o).Max.Y) / 2, bb(o).Min.X, bb(o).Max.X) for o in ox],
[(cy + d, cx - se["half"], cx + se["half"]) for d in se["offsets"]]), f"{len(ox)}")
check(f"bearing {no} {my} level 50 below pedestal top (worst)", 0, max(abs(bb(o).Max.Z - s["z_top"]) for o in oy))
check(f"bearing {no} legs {se['leg']} (worst)", se["leg"], min(bb(o).Max.Z - bb(o).Min.Z for o in oy + ox))
# 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]
info("concrete solids for containment", f"{len(solids)} (coping + pedestals)")
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):
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 coping surface per mark (record; seat bars measured to the coping only)", json.dumps(cover))
write_result(RESULT_PATH, results, dict(cover=cover))
main()