"""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()