"""G1 gate checker for the "single shaft" form (Ramp-A P1, 2026-10-13): drilled shaft, column, Y 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. Two inputs have no dimension text and are drawn values (marked "[작도]" in the item names): the plan end arc corner and the pedestal rotations. Checks: C5 population, C2 closed solids, C1 extents, C4 coping sections (front Y=0, side X=0, plan end), C3 joints and overlaps, C6 volumes (coping integrated from the dimension outline), C7 pedestals / pads. """ 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"] PI, CO, C = G["pile"], G["column"], G["coping"] results = [] # expected outline from the dimensions ZB = CO["z0"] + CO["length"] # column top = coping bottom ZT = ZB + C["soffit_rise"] # tip level (end face bottom) ZTL, ZTR = C["z_top_left"], C["z_top_right"] HB, HT = C["column_face_half"], C["bottom_length"] / 2 TOPH, R = C["top_length"] / 2, C["soffit_radius"] WT, WB = C["width_y"] / 2, C["side_bottom_width"] / 2 PCORNER = C["plan_end_corner_x"] # [작도] def soffit_centre(): """Centre of the R arc through (-HB, ZB) and (-HT, ZT), on the outer (lower-left) side.""" ax, az, bx, bz = -HB, ZB, -HT, ZT 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 # unit normal of the chord c1, c2 = (mx + h * ux, mz + h * uz), (mx - h * ux, mz - h * uz) return min(c1, c2, key=lambda c: c[1]) # the centre below the chord CX, CZ = soffit_centre() def soffit_x(z): """|x| of the soffit at height z (ZB..ZT).""" return abs(CX + math.sqrt(R * R - (z - CZ) ** 2)) def plan_arc(y): sag = HT - PCORNER rr = (WT * WT + sag * sag) / (2 * sag) return HT - (rr - math.sqrt(rr * rr - y * y)) def end_x(y, z, zt): """|x| of the ruled end face at (y, z): plan arc at ZT, straight top edge TOPH at zt.""" t = (z - ZT) / (zt - ZT) return plan_arc(y) + t * (TOPH - plan_arc(y)) 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 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")) def coping_volume_expected(): """Sum over z slices and y strips of the x extent: soffit (below ZT) limited by the end face continued below ZT, end face above ZT; left half up to ZTL, right half up to ZTR.""" total, nz, ny = 0.0, 3000, 200 z1 = max(ZTL, ZTR) dz = (z1 - ZB) / nz for i in range(nz): z = ZB + (i + 0.5) * dz w = WB + (WT - WB) * (z - ZB) / (ZT - ZB) if z < ZT else WT dy = 2 * w / ny area = 0.0 for zt in (ZTL, ZTR): if z > zt: continue for j in range(ny): y = -w + (j + 0.5) * dy xe = end_x(y, z, zt) area += (min(soffit_x(z), xe) if z < ZT else xe) * dy total += area * dz return total def main(): pl = sorted(objs(f"{PID}-PL"), key=lambda o: o.Attributes.GetUserString("id")) counts = {k: len(objs(f"{PID}-{k}")) for k in ("LC", "FT", "CL", "CP", "PL")} flag("C5", "object counts", counts == {"LC": 0, "FT": 0, "CL": 1, "CP": 1, "PL": len(PI["segments"])}, str(counts)) cl, cp = brep(objs(f"{PID}-CL")[0]), brep(objs(f"{PID}-CP")[0]) pls = [brep(o) for o in pl] allb = pls + [cl, cp] flag("C2", f"IsSolid all {len(allb)}", all(b.IsSolid for b in allb), f"{sum(b.IsSolid for b in allb)}/{len(allb)}") # C1 pile segments (bottom to top), column, coping extents z = PI["z_bottom"] for k, (s, b) in enumerate(zip(PI["segments"], pls), 1): bb = bbox(b) check("C1", f"pile segment {k} diameter X", s["d"], bb.Max.X - bb.Min.X) check("C1", f"pile segment {k} diameter Y", s["d"], bb.Max.Y - bb.Min.Y) check("C1", f"pile segment {k} Zbot", z, bb.Min.Z) check("C1", f"pile segment {k} Ztop", z + s["length"], bb.Max.Z) z += s["length"] check("C1", "pile top = EL text", CO["z0"], z) 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", CO["z0"], bb.Min.Z) check("C1", "column Ztop", ZB, bb.Max.Z) bb = bbox(cp) check("C1", "coping length X (bottom tips)", 2 * HT, bb.Max.X - bb.Min.X) check("C1", "coping width Y", 2 * WT, bb.Max.Y - bb.Min.Y) check("C1", "coping Zbot", ZB, 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) check("C1", "total height (pile bottom - coping top left)", G["total_height"], ZTL - PI["z_bottom"]) # C4 front section at Y = 0 def section(plane): ok, curves, pts = rg.Intersect.Intersection.BrepPlane(cp, plane, 0.01) return rg.Curve.JoinCurves(curves, 0.1) sec = section(rg.Plane(rg.Point3d(0, 0, 0), rg.Vector3d.YAxis)) flag("C4", "front section closed single loop", len(sec) == 1 and sec[0].IsClosed, f"{len(sec)} loop(s)") def hits(crv, a, b): return sorted(ev.PointA for ev in rg.Intersect.Intersection.CurveCurve(crv, rg.LineCurve(a, b), 0.01, 0.01)) def z_at(x, upper): zs = sorted(p.Z for p in hits(sec[0], rg.Point3d(x, 0, 0), rg.Point3d(x, 0, 200000))) return zs[-1] if upper else zs[0] for x in (0, HB - 1, -(HB - 1)): check("C4", f"bottom at X={x:+.0f} (column top)", ZB, z_at(x, False)) for x in (1500, -1500, 2500, -2500, 3500, -3500, HT - 5, -(HT - 5)): check("C4", f"soffit R{R:.0f} at X={x:+.0f}", CZ + math.sqrt(R * R - (abs(x) + CX) ** 2), z_at(x, False)) for x in (-3000, -1000, 1000, 3000): check("C4", f"top at X={x:+.0f}", ZTL if x < 0 else ZTR, z_at(x, True)) for x in (-(TOPH + HT) / 2, (TOPH + HT) / 2): zt = ZTL if x < 0 else ZTR check("C4", f"end face at X={x:+.0f} (top {TOPH:.0f} -> bottom {HT:.0f})", ZT + (zt - ZT) * (HT - abs(x)) / (HT - TOPH), z_at(x, True)) check("C4", "top step at the pier centre (right - left)", C["top_step"], z_at(1, True) - z_at(-1, True)) # C4 side section at X = 1 (X = 0 lies on the top step face, 2026-10-13): width 2,000 at the column top -> # 2,200 at the tip level, constant above sec_s = section(rg.Plane(rg.Point3d(1, 0, 0), rg.Vector3d.XAxis)) flag("C4", "side section closed single loop", len(sec_s) == 1 and sec_s[0].IsClosed, f"{len(sec_s)} loop(s)") for zz in (ZB + 1, ZB + 750, ZT - 1, ZT + 500): ys = [p.Y for p in hits(sec_s[0], rg.Point3d(1, -5000, zz), rg.Point3d(1, 5000, zz))] w = WB + (WT - WB) * min(zz - ZB, ZT - ZB) / (ZT - ZB) check("C4", f"side width at Z={zz:.0f}", 2 * w, max(ys) - min(ys) if ys else 0) # C4 plan end (ruled face: convex arc at the tip level, straight top edge) for sx in (-1, 1): zt = ZTL if sx < 0 else ZTR for zz, y in ((ZT + 5, 0), (ZT + 5, WT - 10), ((ZT + zt) / 2, 0), ((ZT + zt) / 2, WT - 10), (zt - 5, WT - 10)): xe = end_x(y, zz, zt) ins = cp.IsPointInside(rg.Point3d(sx * (xe - 3), y, zz), 0.01, False) out = not cp.IsPointInside(rg.Point3d(sx * (xe + 3), y, zz), 0.01, False) flag("C4", f"end face {'left' if sx < 0 else 'right'} at y={y:.0f} z={zz:.0f} [작도 corner {PCORNER:.0f}]", ins and out, f"x_end {sx * xe:.1f}: inside 3 mm in {ins}, outside 3 mm out {out}") # 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 chain = pls + [cl, cp] names = [f"PL{k:02d}" for k in range(1, len(pls) + 1)] + ["CL", "CP"] for (na, a), (nb, b) in zip(zip(names, chain), zip(names[1:], chain[1:])): check("C3", f"{na} top = {nb} bottom", bbox(a).Max.Z, bbox(b).Min.Z, tol=0.01) check("C3", f"overlap {na}-{nb} (mm3)", 0, inter_vol(a, b), tol=1.0) # C6 volumes for k, (s, b) in enumerate(zip(PI["segments"], pls), 1): v = math.pi * (s["d"] / 2000) ** 2 * s["length"] / 1000 check("C6", f"pile segment {k} volume m3", v, vol(b) / 1e9, tol=v * 0.001) cv_ = math.pi * (CO["d"] / 2000) ** 2 * CO["length"] / 1000 check("C6", "column volume m3", cv_, vol(cl) / 1e9, tol=cv_ * 0.001) cpv = coping_volume_expected() / 1e9 check("C6", "coping volume m3 (dimension outline)", cpv, vol(cp) / 1e9, tol=cpv * 0.001) # C7 pedestals and pads S = G["seats"] nos = sorted(int(k) for k in S["table"]) 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)}") p_along, p_across = S["pedestal_along"], S["pedestal_across"] m0, m1 = S["pad_margin"] def bottom_edges(b): zmin = min(v.Location.Z for v in b.Vertices) return [e.PointAtEnd - e.PointAtStart for e in b.Edges if abs(e.PointAtStart.Z - zmin) < 0.01 and abs(e.PointAtEnd.Z - zmin) < 0.01 and e.PointAtStart.DistanceTo(e.PointAtEnd) > 1] 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) ed = bottom_edges(s) al = [v for v in ed if abs(v.Y) > abs(v.X)] ac = [v for v in ed if abs(v.Y) <= abs(v.X)] check("C7", f"NO.{no} pedestal along (C3)", p_along, al[0].Length if al else 0) check("C7", f"NO.{no} pedestal across (C6)", p_across, ac[0].Length if ac else 0) if al: v = al[0] if al[0].Y > 0 else -al[0] check("C7", f"NO.{no} pedestal rotation deg [작도]", S["rotation_deg"][str(no)], math.degrees(math.atan2(v.X, v.Y)), tol=0.05) 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, inter_vol(s, cp), tol=1.0) check("C7", f"NO.{no} overlap pad-pedestal (mm3)", 0, inter_vol(p, s), tol=1.0) corners = [rg.Point3d(q.X, q.Y, sb.Min.Z - 1) for q in [v.Location for v in s.Vertices] if abs(q.Z - sb.Min.Z) < 0.01] flag("C7", f"NO.{no} pedestal footprint on the coping top", all(cp.IsPointInside(q, 0.01, False) for q in corners), f"{len(corners)} corners tested 1 mm below") write_result(RESULT_PATH, results, dict(volumes_m3={**{f"pile{k}": round(vol(b) / 1e9, 3) for k, b in enumerate(pls, 1)}, "column": round(vol(cl) / 1e9, 3), "coping": round(vol(cp) / 1e9, 3)})) main()