"""G3 gate checker, form "shaft cage" (Ramp-A P1, 2026-10-13): drilled-shaft cage bars and the anti-uplift plate. Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G3) come from the D4 schedule (L, N), the 구조물 치수표 chains measured from the shaft top EL (D1), the section radii and the user decisions S1-S6; generator params are not read. Checks (C9): counts, original length (+coupler) vs schedule L, main radius / division / piece levels, hoop and tie level grids, hoop radius, X / Y ties apart, U bars (count, bottom level, width, planes), plate (size, thickness, centre, cover), containment in the pile segments, min side cover (record). """ import json import math import os 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, TOL, G = X["pier_id"], X["tol_mm"], X["G3"] SCHED_L, SCHED_N, DIA = G["schedule_L"], G["schedule_n"], G["dia"] M, HP, TI, U, PL = G["main"], G["hoops"], G["ties"], G["u_bars"], G["plate"] TOP = G["shaft_top"] PILE = X["G1"]["pile"] Z_BOT = PILE["z_bottom"] SEG = [] # (z0, z1, radius) bottom to top z = Z_BOT for s in PILE["segments"]: SEG.append((z, z + s["length"], s["d"] / 2)) z += s["length"] def chain(c): """Levels from the shaft top: first offset, then (count, pitch) groups downwards; returned bottom to top.""" out = [TOP - c["first"]] for n, p in c["groups"]: out += [out[-1] - p * k for k in range(1, n + 1)] return sorted(out) HOOP_LEVELS, TIE_LEVELS = chain(HP["chain"]), chain(TI["chain"]) results = [] def check(item, exp, act, tol=TOL): 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 ids(prefix): return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)] def main(): cl = {m: [o for o in ids(f"{PID}-RC-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L} pipes = {m: [o for o in ids(f"{PID}-R-") if o.Attributes.GetUserString("mark") == m] for m in SCHED_L} occ = lambda o: o.Attributes.GetUserString("occurrence").rstrip("ab") hm, tm = HP["mark"], TI["mark"] for m in SCHED_L: n_occ = len({occ(o) for o in cl[m]}) if m == hm else len(cl[m]) check(f"{m} bar count", SCHED_N[m], n_occ, tol=0) flag(f"{m} pipes closed & paired with centrelines", len(pipes[m]) == len(cl[m]) and all(o.Geometry.IsSolid for o in pipes[m]), f"{len(pipes[m])}/{len(cl[m])}") for m in SCHED_L: per, curve = {}, {} for o in cl[m]: k = occ(o) if m == hm else o.Attributes.GetUserString("occurrence") cp = float(o.Attributes.GetUserString("coupler_length") or 0) per[k] = per.get(k, 0) + float(o.Attributes.GetUserString("design_length")) + cp curve[k] = curve.get(k, 0) + o.Geometry.GetLength() + cp if per: check(f"{m} original length (+coupler) vs schedule L (worst of {len(per)})", 0, max(abs(v - SCHED_L[m]) for v in per.values())) dev = [v - SCHED_L[m] for v in curve.values()] info(f"{m} curve length (+coupler) - L (record)", f"min {min(dev):.1f} / max {max(dev):.1f} mm") # main bars: pieces stacked between the first and the last hoop level (S6: coupler length 0) zb, zt = HOOP_LEVELS[0], HOOP_LEVELS[-1] check("main span (last - first hoop level) = sum of piece L", zt - zb, sum(SCHED_L[mk] for mk in M["pieces"])) z = zb for mk in M["pieces"]: # bottom to top lo = [min(o.Geometry.PointAtStart.Z, o.Geometry.PointAtEnd.Z) for o in cl[mk]] hi = [max(o.Geometry.PointAtStart.Z, o.Geometry.PointAtEnd.Z) for o in cl[mk]] check(f"{mk} bottom EL (worst)", 0, max(abs(v - z) for v in lo)) check(f"{mk} top EL = bottom + L (worst)", 0, max(abs(v - (z + SCHED_L[mk])) for v in hi)) z += SCHED_L[mk] rr = [math.hypot(o.Geometry.PointAtStart.X, o.Geometry.PointAtStart.Y) for o in cl[mk]] check(f"{mk} radius (worst)", 0, max(abs(r - M["radius"]) for r in rr), tol=0.5) a = sorted(math.degrees(math.atan2(o.Geometry.PointAtStart.Y, o.Geometry.PointAtStart.X)) % 360 for o in cl[mk]) check(f"{mk} angle vs {M['start_deg']} + i*360/{M['divisions']} (worst, deg)", 0, max(abs(x - (M["start_deg"] + i * 360 / M["divisions"])) for i, x in enumerate(a)), tol=0.01) # hoops hz = {} for o in cl[hm]: bb = o.Geometry.GetBoundingBox(True) hz.setdefault(round((bb.Max.Z + bb.Min.Z) / 2, 1), []).append(o) zs = sorted(hz) flag(f"{hm} levels {len(HOOP_LEVELS)} x 2 pieces", len(zs) == len(HOOP_LEVELS) and all(len(v) == 2 for v in hz.values()), f"{len(zs)} levels") check(f"{hm} levels vs 구조물 치수표 chain (worst)", 0, max(abs(a - b) for a, b in zip(zs, HOOP_LEVELS)) if len(zs) == len(HOOP_LEVELS) else 1e9) for o in cl[hm][:4]: c = o.Geometry p = c.PointAt(c.Domain.Mid) check(f"{hm} radius at mid point ({o.Attributes.GetUserString('occurrence')})", HP["radius"], math.hypot(p.X, p.Y), tol=0.5) # ties by = lambda s: sorted((o.Geometry for o in cl[tm] if o.Attributes.GetUserString("occurrence").endswith(s)), key=lambda g: g.GetBoundingBox(True).Max.Z) xs, ys = by("X"), by("Y") txm = [g.GetBoundingBox(True).Max.Z for g in xs] tym = [g.GetBoundingBox(True).Max.Z for g in ys] flag(f"{tm} X / Y per level, {len(TIE_LEVELS)} levels", len(txm) == len(tym) == len(TIE_LEVELS), f"X {len(txm)} Y {len(tym)}") if len(txm) == len(TIE_LEVELS) == len(tym): check(f"{tm} X levels vs chain (worst)", 0, max(abs(a - b) for a, b in zip(txm, TIE_LEVELS))) check(f"{tm} Y levels = X + {TI['y_lift']} (worst)", 0, max(abs(a - b - TI["y_lift"]) for a, b in zip(tym, TIE_LEVELS))) gaps = [] for a, b in zip(xs, ys): ok, pa, pb = a.ClosestPoints(b) gaps.append(pa.DistanceTo(pb) if ok else 0.0) gap = min(gaps) if gaps else 0.0 check(f"{tm} X / Y centrelines apart >= bar diameter {DIA[tm]} (min, capped)", DIA[tm], min(gap, DIA[tm]), tol=0.01) # U bars: bottom level, overall width (vertical legs on the hoop circle), one plane each through the centre um = U["mark"] bots = [o.Geometry.GetBoundingBox(True).Min.Z for o in cl[um]] check(f"{um} bottom EL = shaft bottom + {U['bottom_above']} (worst, centreline)", 0, max(abs(b - (Z_BOT + U["bottom_above"])) for b in bots) if bots else 1e9) w, planar = [], 0 for o in cl[um]: c = o.Geometry a, b = c.PointAtStart, c.PointAtEnd w.append(math.hypot(a.X - b.X, a.Y - b.Y)) ok, pl = c.TryGetPlane(0.01) if ok and abs(pl.Normal.Z) < 1e-6 and abs(pl.DistanceTo(rg.Point3d(0, 0, pl.Origin.Z))) < 0.5: planar += 1 check(f"{um} leg spacing (worst) = 2 x hoop radius {2 * HP['radius']:.0f}", 0, max(abs(x - 2 * HP["radius"]) for x in w) if w else 1e9) check(f"{um} vertical planes through the centre", len(cl[um]), planar, tol=0) # plate pts = ids(f"{PID}-PT") flag("plate count 1", len(pts) == 1, str(len(pts))) if pts: bb = pts[0].Geometry.GetBoundingBox(True) check("plate size X", PL["size"], bb.Max.X - bb.Min.X) check("plate size Y", PL["size"], bb.Max.Y - bb.Min.Y) check("plate thickness", PL["thickness"], bb.Max.Z - bb.Min.Z, tol=0.01) check("plate centre X / Y offset", 0, math.hypot((bb.Max.X + bb.Min.X) / 2, (bb.Max.Y + bb.Min.Y) / 2)) check(f"plate top = U bottom - {DIA[um] / 2:.0f} (S4)", Z_BOT + U["bottom_above"] - DIA[um] / 2, bb.Max.Z, tol=0.01) info("plate bottom cover (record)", f"{bb.Min.Z - Z_BOT:.1f} mm") # containment and side cover in the pile segments solids = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-PL")] min_cover = {} for m in SCHED_L: 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() / 150)), True): p = c.PointAt(t) radial = rg.Vector3d(p.X, p.Y, 0) radial.Unitize() if radial.Length > 1e-6 else None for q in (p, p + radial * r, p - radial * r, 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 R = next((rad for z0, z1, rad in SEG if z0 <= p.Z <= z1), None) if R is not None: cov = min(cov, R - math.hypot(p.X, p.Y) - r, p.Z - Z_BOT - r) min_cover[m] = round(cov, 1) check(f"{m} sample points outside the pile concrete", 0, bad, tol=0) info("min cover per mark (side or bottom, record)", json.dumps(min_cover)) write_result(RESULT_PATH, results, dict(min_cover=min_cover)) main()