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