"""G4 gate checker, Ramp-A P1 coping rebar (2026-10-14): C1~C16 and bearing-seat S1~S4 as centreline polylines. Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4, plus the G1 coping dimensions) come from the D3 schedule (L, N), the D2 / D3 drawn positions and section dimensions, and the user decisions T1-T6; generator params are not read. Checks (C10): counts, fixed-length marks vs L (variable marks recorded), top layers (level, y rows, x ends), stirrup positions per zone, stirrup top / bottom cover along the soffit, inner stirrup offset, side loops per level, C6 bottom level, C15 / C16, seat bars per bearing, containment in the coping + pedestals, min cover (record). """ import json import math 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, C, CO = X["pier_id"], X["G4"], X["G1"]["coping"], X["G1"]["column"] SCHED = {m: tuple(v) for m, v in G["schedule"].items()} ZB = CO["z0"] + CO["length"] # column top = coping bottom ZT = ZB + C["soffit_rise"] ZTL, ZTR = C["z_top_left"], C["z_top_right"] R, HB, HT = C["soffit_radius"], C["column_face_half"], C["bottom_length"] / 2 results = [] def soffit_centre(): ax, az, bx, bz = -HB, ZB, -HT, ZT # R arc through (-HB, ZB) and (-HT, ZT), centre below the chord mx, mz = (ax + bx) / 2, (az + bz) / 2 d = math.hypot(bx - ax, bz - az) h = math.sqrt(R * R - d * d / 4) ux, uz = (bz - az) / d, -(bx - ax) / d return min(((mx + h * ux, mz + h * uz), (mx - h * ux, mz - h * uz)), key=lambda c: c[1]) SCX, SCZ = soffit_centre() SCX = abs(SCX) def soffit_z(x): return ZB if abs(x) <= HB else SCZ + math.sqrt(R * R - (SCX - abs(x)) ** 2) 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(s): return [s["start"] + s["pitch"] * i for i in range(s["n"])] def bb(o): return o.Geometry.GetBoundingBox(True) def worst(pairs): pairs = list(pairs) return max((abs(a - b) for a, b in pairs), default=1e9) 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) 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])}") dl = [float(o.Attributes.GetUserString("design_length")) for o in cl[m]] if not dl: continue if m in G["fixed_length"]: check(f"{m} original length vs L (worst)", 0, max(abs(v - L) for v in dl)) else: info(f"{m} mean original length vs L (variable mark, record)", f"mean {statistics.mean(dl):.1f} / L {L} (diff {statistics.mean(dl) - L:+.1f}); min {min(dl):.0f} max {max(dl):.0f}") # top layers (straight part = top of the bar) for m, s in G["layers"].items(): got = sorted(round((bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[m]) want = row(s["y"]) check(f"{m} y rows (worst, {len(got)}/{len(want)})", 0, worst(zip(got, want)) if len(got) == len(want) else 1e9) check(f"{m} level of the straight part (worst)", 0, max(abs(bb(o).Max.Z - s["z"]) for o in cl[m])) if "x_top" in s: x0, x1 = s["x_top"] ends = [] for o in cl[m]: c = o.Geometry top_pts = [p for p in (c.PointAt(t) for t in c.DivideByLength(5, True)) if abs(p.Z - s["z"]) < 0.5] ends.append(max(abs(min(p.X for p in top_pts) - x0), abs(max(p.X for p in top_pts) - x1))) check(f"{m} straight part x ends vs drawn (worst; fillet tangent points)", 0, max(ends), tol=s.get("x_tol", 70)) # stirrups st = G["stirrups"] xs = row(st["x"]) for zone, (m_out, m_in, x0, x1) in st["zones"].items(): want = [x for x in xs if x0 <= x <= x1] got_o = sorted(round(bb(o).Min.X, 1) for o in cl[m_out]) got_i = sorted(round(bb(o).Min.X, 1) for o in cl[m_in]) check(f"{m_out} positions = drawn x ({len(want)})", 0, worst(zip(got_o, want)) if len(got_o) == len(want) else 1e9) check(f"{m_in} positions = drawn x + {st['inner_dx']} (2 per x)", 0, worst(zip(got_i, sorted(x + st["inner_dx"] for x in want for _ in (0, 1)))) if len(got_i) == 2 * len(want) else 1e9) for m in (m_out, m_in): dt, dbm = [], [] for o in cl[m]: b = bb(o) x = b.Min.X - (st["inner_dx"] if m == m_in else 0) t = ZTL if x <= 0 else ZTR dt.append(abs((t - b.Max.Z) - st["top_cover"])) dbm.append(abs((b.Min.Z - soffit_z(x)) - st["bottom_cover"] * (1 if abs(x) < HB else # on the arc the 77.5 cover is radial: vertical gap = R' - R projected (SCZ + math.sqrt((R + st["bottom_cover"]) ** 2 - (SCX - abs(x)) ** 2) - soffit_z(x)) / st["bottom_cover"]))) check(f"{m} top cover {st['top_cover']} (worst)", 0, max(dt)) check(f"{m} bottom cover {st['bottom_cover']} to the soffit (worst)", 0, max(dbm)) ys = [(bb(o).Min.Y, bb(o).Max.Y) for o in cl[m_out]] check(f"{m_out} legs y {st['outer_y']} (worst)", 0, worst([(a, st["outer_y"][0]) for a, _ in ys] + [(b, st["outer_y"][1]) for _, b in ys])) # side loops lp = G["loops"] for m, levels in lp["levels"].items(): zs = {} for o in cl[m]: zs.setdefault(round(bb(o).Min.Z, 1), []).append(o) flag(f"{m} levels {levels} x {lp['per_level']}", sorted(zs) == sorted(levels) and all(len(v) == lp["per_level"] for v in zs.values()), f"{sorted(zs)}") flat = [abs(bb(o).Max.Z - bb(o).Min.Z) for o in cl[m]] check(f"{m} loops horizontal (worst dz)", 0, max(flat) if flat else 1e9, tol=0.01) # C6, C15, C16 c6 = G["c6"] check("C6 bottom straight level (worst)", 0, max(abs(bb(o).Min.Z - c6["z_bottom"]) for o in cl["C6"]) if cl["C6"] else 1e9) got = sorted(round(o.Geometry.PointAt(o.Geometry.Domain.Mid).Y, 1) for o in cl["C6"]) check("C6 y rows at mid-span (worst)", 0, worst(zip(got, c6["y"])) if len(got) == len(c6["y"]) else 1e9) for m in ("C15",): w = [bb(o).Max.Y - bb(o).Min.Y for o in cl[m]] check(f"{m} width a (worst)", G["c15"]["a"], max(w, key=lambda v: abs(v - G["c15"]["a"])) if w else 0) # seats for no, want in G["seats"].items(): for m, n in want.items(): k = sum(1 for o in cl[m] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}-")) check(f"bearing {no} {m} count", n, k, tol=0) # 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] col = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-CL"] info("concrete solids for containment", f"{len(solids)} (coping + pedestals); bars below the coping bottom checked in the column") 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 + col): 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 the coping surface per mark (record; seat bars measured to the coping only)", json.dumps(cover)) write_result(RESULT_PATH, results, dict(cover=cover)) main()