"""G1 gate checker for the "tapered T" form (Ulsan P1, 2026-10-13): footing, lean, column, coping, pedestals, pads. Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G1 of EXPECTED_PATH) come from drawing dimension texts / EL texts / tables; generator params are not read. Optional (Ulsan P2, 2026-10-15; absent -> the checks are unchanged): G1.footing.rot_deg (foundation drawn in its own axes, rotated counter-clockwise about the pier centre; lean / footing / piles are checked after rotating them back and their rotated plan extents are compared with the closed form), G1.piles {nx, ny, pitch_x, pitch_y, edge_x, edge_y, d, length, head_embedment}. Checks: C5 population, C2 closed solids, C1 extents, C4 coping section at Y=0 and plan corner rounding, C3 joints and overlaps, C6 volumes (coping: plan section areas integrated over Z from the dimensions, R corner fillets in closed form), C7 pedestals / pads per bearing. """ 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["G1"] F, LN, CO, C, PI = G["footing"], G["lean"], G["column"], G["coping"], G.get("piles") ROT = F.get("rot_deg", 0.0) FT_TOP = F["z0"] + F["thickness"] Z_CB = FT_TOP + CO["height"] # column top = coping bottom results = [] def objs(prefix): return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)] def brep(o): return o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry) def vol(b): return rg.VolumeMassProperties.Compute(b).Volume def bbox(b): return b.GetBoundingBox(True) def unrot(b): """Copy of b rotated back by the foundation rotation (identity when the foundation is not rotated).""" b = b.DuplicateBrep() if ROT: b.Transform(rg.Transform.Rotation(-math.radians(ROT), rg.Vector3d.ZAxis, rg.Point3d.Origin)) return b def check(cid, name, expected, actual, tol=TOL): results.append(dict(check=cid, item=name, expected=round(expected, 3), actual=round(actual, 3), diff=round(actual - expected, 3), tol=tol, result="PASS" if abs(actual - expected) <= tol else "FAIL")) def flag(cid, name, ok, detail=""): results.append(dict(check=cid, item=name, detail=detail, result="PASS" if ok else "FAIL")) # expected coping outline from the dimensions (front view, Y = 0) FLAT, TIP, TOPH, RISE = C["bottom_flat_half"], C["tip_half"], C["top_half"], C["soffit_rise"] ZTL, ZTR = C["z_top_left"], C["z_top_right"] Z_TIP = Z_CB + RISE def bottom_z(x): ax = abs(x) return Z_CB if ax <= FLAT else Z_CB + RISE * (ax - FLAT) / (TIP - FLAT) def top_z(x): """Upper outline: top level of the side, then the battered end face from top_half down to the tip.""" zt = ZTL if x < 0 else ZTR ax = abs(x) if ax <= TOPH: return zt return zt - (zt - Z_TIP) * (ax - TOPH) / (TIP - TOPH) def coping_volume_expected(): """Integrate the plan section area over Z. At height z each side reaches x_end(z): the soffit line below the tip level (plan outline: tip length, corner radius R), the battered end face above it (rounded outline shrinking from the tip length to the top length). Corner cuts use the closed form of the R fillet area.""" W, R = C["width_y"], C["plan_corner_radius"] def cut_area(e): # area cut by one R fillet over 0..e beyond its start e = min(max(e, 0.0), R) return R * e - (e / 2 * math.sqrt(R * R - e * e) + R * R / 2 * math.asin(e / R)) total, n = 0.0, 40000 z0, z1 = Z_CB, max(ZTL, ZTR) dz = (z1 - z0) / n for i in range(n): z = z0 + (i + 0.5) * dz area = 0.0 for zt in (ZTL, ZTR): if z > zt: continue if z < Z_TIP: x_end = FLAT + (TIP - FLAT) * (z - Z_CB) / RISE # soffit line rect_end = TIP else: x_end = TIP - (TIP - TOPH) * (z - Z_TIP) / (zt - Z_TIP) rect_end = x_end area += W * x_end - 2 * cut_area(x_end - (rect_end - R)) total += area * dz return total def main(): ids = {k: f"{PID}-{k}" for k in ("LC", "FT", "CL", "CP", "PL")} S = G["seats"] nos = sorted(int(k) for k in S["table"]) counts = {k: len(objs(v)) for k, v in ids.items()} flag("C5", "object counts", counts == {"LC": 1, "FT": 1, "CL": 1, "CP": 1, "PL": PI["nx"] * PI["ny"] if PI else 0}, str(counts)) lc, ft, cl, cp = (brep(objs(ids[k])[0]) for k in ("LC", "FT", "CL", "CP")) piles = [brep(o) for o in objs(ids["PL"])] allb = [lc, ft, cl, cp] + piles flag("C2", f"IsSolid all {len(allb)}", all(b.IsSolid for b in allb), f"{sum(b.IsSolid for b in allb)}/{len(allb)}") for key, b, dx, dy, z0, z1 in (("footing", ft, F["dx"], F["dy"], F["z0"], FT_TOP), ("lean", lc, F["dx"] + 2 * LN["margin"], F["dy"] + 2 * LN["margin"], F["z0"] - LN["thickness"], F["z0"])): if ROT: # raw plan extent of the rotated rectangle (closed form) t = math.radians(abs(ROT)) rb = bbox(b) check("C1", f"{key} rotated extent X", dx * math.cos(t) + dy * math.sin(t), rb.Max.X - rb.Min.X) check("C1", f"{key} rotated extent Y", dx * math.sin(t) + dy * math.cos(t), rb.Max.Y - rb.Min.Y) bb = bbox(unrot(b)) check("C1", f"{key} X", dx, bb.Max.X - bb.Min.X) check("C1", f"{key} Y", dy, bb.Max.Y - bb.Min.Y) check("C1", f"{key} Zbot", z0, bb.Min.Z) check("C1", f"{key} Ztop", z1, bb.Max.Z) check("C1", f"{key} centre X", 0, (bb.Max.X + bb.Min.X) / 2) check("C1", f"{key} centre Y", 0, (bb.Max.Y + bb.Min.Y) / 2) bb = bbox(cl) check("C1", "column diameter X", CO["d"], bb.Max.X - bb.Min.X) check("C1", "column diameter Y", CO["d"], bb.Max.Y - bb.Min.Y) check("C1", "column Zbot", FT_TOP, bb.Min.Z) check("C1", "column Ztop", Z_CB, bb.Max.Z) if PI and piles: pr = [bbox(unrot(b)) for b in piles] xs = sorted({round((q.Min.X + q.Max.X) / 2, 1) for q in pr}) ys = sorted({round((q.Min.Y + q.Max.Y) / 2, 1) for q in pr}) flag("C1", f"pile grid {PI['nx']} x {PI['ny']}", len(xs) == PI["nx"] and len(ys) == PI["ny"], f"x={xs} y={ys}") if len(xs) == PI["nx"] and len(ys) == PI["ny"]: check("C1", "pile edge X", PI["edge_x"], xs[0] + F["dx"] / 2) check("C1", "pile edge Y", PI["edge_y"], ys[0] + F["dy"] / 2) check("C1", "pile pitch X (worst)", 0, max(abs(b - a - PI["pitch_x"]) for a, b in zip(xs, xs[1:]))) check("C1", "pile pitch Y (worst)", 0, max(abs(b - a - PI["pitch_y"]) for a, b in zip(ys, ys[1:]))) check("C1", "pile diameter", PI["d"], pr[0].Max.X - pr[0].Min.X) check("C1", "pile length", PI["length"], pr[0].Max.Z - pr[0].Min.Z) check("C1", "pile head embedment", PI["head_embedment"], pr[0].Max.Z - F["z0"]) bb = bbox(cp) check("C1", "coping length X (tip to tip)", 2 * TIP, bb.Max.X - bb.Min.X) check("C1", "coping width Y", C["width_y"], bb.Max.Y - bb.Min.Y) check("C1", "coping Zbot", Z_CB, bb.Min.Z) check("C1", "coping Ztop", max(ZTL, ZTR), bb.Max.Z) check("C1", "coping centre X", 0, (bb.Max.X + bb.Min.X) / 2) # C4 section at Y = 0 against the dimension outline ok, curves, pts = rg.Intersect.Intersection.BrepPlane(cp, rg.Plane(rg.Point3d(0, 0, 0), rg.Vector3d.YAxis), 0.01) sec = rg.Curve.JoinCurves(curves, 0.1) flag("C4", "section closed single loop", len(sec) == 1 and sec[0].IsClosed, f"{len(sec)} loop(s)") def z_at(x, upper): line = rg.LineCurve(rg.Point3d(x, 0, 0), rg.Point3d(x, 0, 200000)) zs = sorted(ev.PointA.Z for ev in rg.Intersect.Intersection.CurveCurve(sec[0], line, 0.01, 0.01)) return zs[-1] if upper else zs[0] for x in (0, FLAT - 1, -(FLAT - 1), 3000, -3000, TIP - 5, -(TIP - 5)): check("C4", f"bottom at X={x:+.0f}", bottom_z(x), z_at(x, False)) for x in (-2000, 2000, -(TOPH - 10), TOPH - 10, -(TOPH + TIP) / 2, (TOPH + TIP) / 2): check("C4", f"top at X={x:+.0f}", top_z(x), z_at(x, True)) check("C4", "top step at the pier centre (left - right)", ZTL - ZTR, z_at(-1, True) - z_at(1, True)) # plan corner rounding (R) just above the tips R, W = C["plan_corner_radius"], C["width_y"] zc = Z_TIP + 50 inside = lambda x, y: cp.IsPointInside(rg.Point3d(x, y, zc), 0.01, False) # test point on the 45 deg diagonal from the fillet centre, half way between the arc (R) and the sharp corner d = (R + R * math.sqrt(2)) / 2 / math.sqrt(2) corner_x, corner_y = TIP - R + d, W / 2 - R + d flag("C4", f"plan corners rounded R{R} (4 corners outside, edges inside)", all(not inside(sx * corner_x, sy * corner_y) for sx in (1, -1) for sy in (1, -1)) and all(inside(sx * (TIP - R - 50), sy * (W / 2 - 10)) for sx in (1, -1) for sy in (1, -1)), f"z {zc}, test point ({corner_x:.0f}, {corner_y:.0f})") # C3 joints and overlaps def inter_vol(a, b): r = rg.Brep.CreateBooleanIntersection(a, b, 0.01) return sum(vol(x) for x in r) if r else 0.0 check("C3", "LC top = FT bottom", bbox(lc).Max.Z, bbox(ft).Min.Z, tol=0.01) check("C3", "FT top = CL bottom", bbox(ft).Max.Z, bbox(cl).Min.Z, tol=0.01) check("C3", "CL top = CP bottom", bbox(cl).Max.Z, bbox(cp).Min.Z, tol=0.01) check("C3", "overlap LC-FT (mm3)", 0, inter_vol(lc, ft), tol=1.0) check("C3", "overlap FT-CL (mm3)", 0, inter_vol(ft, cl), tol=1.0) check("C3", "overlap CL-CP (mm3)", 0, inter_vol(cl, cp), tol=1.0) if PI: check("C3", "overlap LC-piles (mm3)", 0, sum(inter_vol(lc, b) for b in piles), tol=1.0) emb = len(piles) * math.pi * (PI["d"] / 2) ** 2 * PI["head_embedment"] check("C3", "intended overlap FT-piles (mm3)", emb, sum(inter_vol(ft, b) for b in piles), tol=emb * 0.001) flag("C3", "column within coping bottom flat zone", CO["d"] / 2 <= FLAT and CO["d"] / 2 <= W / 2, f"r={CO['d'] / 2:.0f} <= {FLAT:.0f} (X), {W / 2:.0f} (Y)") # C6 volumes fv = F["dx"] * F["dy"] * F["thickness"] / 1e9 check("C6", "footing volume m3", fv, vol(ft) / 1e9, tol=fv * 0.001) cv_ = math.pi * (CO["d"] / 2000) ** 2 * CO["height"] / 1000 check("C6", "column volume m3", cv_, vol(cl) / 1e9, tol=cv_ * 0.001) holes = len(piles) * math.pi * (PI["d"] / 2) ** 2 if PI else 0 lv = ((F["dx"] + 2 * LN["margin"]) * (F["dy"] + 2 * LN["margin"]) - holes) * LN["thickness"] / 1e9 check("C6", "lean volume m3 (minus pile holes)", lv, vol(lc) / 1e9, tol=lv * 0.001) cpv = coping_volume_expected() / 1e9 check("C6", "coping volume m3 (dimension outline, R corners)", cpv, vol(cp) / 1e9, tol=cpv * 0.001) # C7 pedestals and pads n0 = len(PID) + 3 seats = {int(o.Attributes.GetUserString("id")[n0:]): brep(o) for o in objs(f"{PID}-BS")} pads = {int(o.Attributes.GetUserString("id")[n0:]): brep(o) for o in objs(f"{PID}-BP")} flag("C7", "pedestal / pad counts", sorted(seats) == nos and sorted(pads) == nos, f"seats={len(seats)} pads={len(pads)}") allb = list(seats.values()) + list(pads.values()) flag("C7", "IsSolid pedestals+pads", all(b.IsSolid for b in allb), f"{sum(b.IsSolid for b in allb)}/{len(allb)}") px, py = S["pedestal"] m0, m1 = S["pad_margin"] for no in nos: elx, h1, h2, a, b = S["table"][str(no)] x, y = S["pos"][str(no)] top = ZTL if x < 0 else ZTR s, p = seats[no], pads[no] sb, pb = bbox(s), bbox(p) check("C7", f"NO.{no} table EL.'X' - h1 - h2 = coping top ({'left' if x < 0 else 'right'})", top, elx - h1 - h2) check("C7", f"NO.{no} pedestal bottom = coping top", top, sb.Min.Z) check("C7", f"NO.{no} pedestal top = top + h1", top + h1, sb.Max.Z) check("C7", f"NO.{no} pad top = EL.'X'", elx, pb.Max.Z) check("C7", f"NO.{no} pedestal size X", px, sb.Max.X - sb.Min.X) check("C7", f"NO.{no} pedestal size Y", py, sb.Max.Y - sb.Min.Y) check("C7", f"NO.{no} pedestal centre X", x, (sb.Max.X + sb.Min.X) / 2) check("C7", f"NO.{no} pedestal centre Y", y, (sb.Max.Y + sb.Min.Y) / 2) c = rg.VolumeMassProperties.Compute(p).Centroid check("C7", f"NO.{no} pad centre X", x, c.X) check("C7", f"NO.{no} pad centre Y", y, c.Y) mm_ = (m0 + m1) / 2 pv = h2 / 6 * ((a + m0) * (b + m0) + (a + m1) * (b + m1) + 4 * (a + mm_) * (b + mm_)) check("C7", f"NO.{no} pad volume (prismatoid A,B)", pv, vol(p), tol=pv * 0.001) check("C7", f"NO.{no} overlap pedestal-coping (mm3)", 0, sum(vol(q) for q in (rg.Brep.CreateBooleanIntersection(s, cp, 0.01) or [])), tol=1.0) check("C7", f"NO.{no} overlap pad-pedestal (mm3)", 0, sum(vol(q) for q in (rg.Brep.CreateBooleanIntersection(p, s, 0.01) or [])), tol=1.0) write_result(RESULT_PATH, results, dict(volumes_m3={k: round(vol(b) / 1e9, 3) for k, b in (("lean", lc), ("footing", ft), ("column", cl), ("coping", cp))})) main()