chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합

- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ahnyongjooandClaude Sonnet 5.5 committed 2026-10-28 11:20:18 +09:00
commit 964a841de1
3978 files changed
+58013356

No files matched your search

@@ -0,0 +1,62 @@
"""Shared helpers for the gate checkers (imported inside Rhino 8).
Checkers read only EXPECTED_PATH (independent drawing values) and never the generator params.
"""
import json
from Rhino.DocObjects import ObjectEnumeratorSettings
def load_expected(path):
return json.load(open(path, encoding="utf-8"))
def all_objects(doc):
"""All live objects incl. hidden/locked (doc.Objects alone skips hidden ones)."""
s = ObjectEnumeratorSettings()
s.HiddenObjects = True
s.LockedObjects = True
return list(doc.Objects.GetObjectList(s))
def chain(spec, base=0.0):
"""Expand a dimension chain: start offsets (cumulative) then groups [[n, pitch], ...] from the last point."""
pts, z = [], base
for s in spec["start"]:
z += s
pts.append(z)
for n, p in spec["groups"]:
for _ in range(n):
z += p
pts.append(z)
return pts
def series(spec):
"""Expand {start, pitch, n} (or a literal list) into a list of values."""
if isinstance(spec, list):
return list(spec)
return [spec["start"] + spec["pitch"] * i for i in range(spec["n"])]
def row(spec):
"""Bar positions: two end bars plus a regular series."""
return sorted(list(spec["ends"]) + series(spec))
def ychain(spec, half):
"""Section chain measured from the -Y face: listed first bars (absolute), then n x pitch from the last of
them, then the last bar at `end`; returned relative to the section centre (minus `half`)."""
pts = list(spec["start"])
pts += [pts[-1] + spec["pitch"] * k for k in range(1, spec["n"] + 1)] + [spec["end"]]
return [y - half for y in sorted(pts)]
def write_result(path, results, extra=None):
summ = dict(total=len([r for r in results if r["result"] != "INFO"]),
passed=sum(r["result"] == "PASS" for r in results),
failed=sum(r["result"] == "FAIL" for r in results))
summ.update(extra or {})
json.dump(dict(summary=summ, results=results), open(path, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
print(json.dumps(summ, ensure_ascii=False))
return summ
@@ -0,0 +1,217 @@
"""G1 gate checker, form "tapered T with shared pedestals": piles and plan skew optional (unified 2026-10-15 from g1_skew.py,
Okseong P1, and this file, Saejae-1 P1): lean, footing, column, coping (straight soffit, stepped top, side taper), shared pedestals
with a sloped top, pads. G1 expected "piles" absent = spread footing (no pile checks); coping.skew_deg 0 = rectangular plan.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G1) come from dimension / EL texts and
the bearing tables; the skew angle and the pedestal outlines are drawn values ([작도]). Generator params are not read.
Checks: C5 population, C2 closed solids, C1 extents (lean, footing, column, piles), C4 coping (front section at Y=0,
side width at X=0, skewed end faces), C3 joints / overlaps, C6 volumes, 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"]
F, LN, CO, C, PI = G["footing"], G["lean"], G["column"], G["coping"], G.get("piles")
FT_TOP = F["z0"] + F["thickness"]
ZB = FT_TOP + CO["height"] # column top = coping bottom
ZT = ZB + C["soffit_rise"]
FLAT, TIP, W, WB = C["bottom_flat_half"], C["tip_half"], C["width_y"] / 2, C["side_bottom_width"] / 2
K = math.tan(math.radians(C["skew_deg"])) # [작도]
EDGES = [-TIP] + C["step_bounds"] + [TIP] # top step boundaries at Y = 0 (front dimension chain)
STEPS = C["step_z"] # step levels (EL.'X' - h1 - h2 of the bearing rows)
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 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 top_z(x):
for a, b, z in zip(EDGES, EDGES[1:], STEPS):
if a <= x <= b:
return z
return None
def bottom_z(x):
ax = abs(x)
return ZB if ax <= FLAT else ZB + C["soffit_rise"] * (ax - FLAT) / (TIP - FLAT)
def coping_volume_expected():
"""Shear keeps the volume: integrate the unsheared plan area (front x extent x side width) over z."""
total, n = 0.0, 20000
z1 = max(STEPS)
dz = (z1 - ZB) / n
for i in range(n):
z = ZB + (i + 0.5) * dz
if z < ZT:
xh = FLAT + (TIP - FLAT) * (z - ZB) / (ZT - ZB)
yh = WB + (W - WB) * (z - ZB) / (ZT - ZB)
total += 2 * xh * 2 * yh * dz
else:
length = sum(b - a for a, b, s in zip(EDGES, EDGES[1:], STEPS) if s >= z)
total += length * 2 * W * dz
return total
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
def main():
ids = {k: f"{PID}-{k}" for k in ("LC", "FT", "CL", "CP", "PL")}
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)}")
# C1 extents
for key, b, dx, dy, z0, z1 in (("footing", ft, F["dx"], F["dy"], F["z0"], FT_TOP),
("lean", lc, LN["dx"], LN["dy"], F["z0"] - LN["thickness"], F["z0"])):
bb = bbox(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", ZB, bb.Max.Z)
xs = sorted({round((bbox(b).Min.X + bbox(b).Max.X) / 2, 1) for b in piles})
ys = sorted({round((bbox(b).Min.Y + bbox(b).Max.Y) / 2, 1) for b in piles})
if PI:
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 PI and xs and ys:
check("C1", "pile edge X", PI["edge"], xs[0] + F["dx"] / 2)
check("C1", "pile edge Y", PI["edge"], ys[0] + F["dy"] / 2)
check("C1", "pile pitch X (worst)", 0, max(abs(b - a - PI["pitch"]) for a, b in zip(xs, xs[1:])))
check("C1", "pile pitch Y (worst)", 0, max(abs(b - a - PI["pitch"]) for a, b in zip(ys, ys[1:])))
pb = bbox(piles[0])
check("C1", "pile diameter", PI["d"], pb.Max.X - pb.Min.X)
check("C1", "pile length", PI["length"], pb.Max.Z - pb.Min.Z)
check("C1", "pile head embedment", PI["head_embedment"], pb.Max.Z - F["z0"])
# C4 coping: front section at Y = 0
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", "front section (Y=0) 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), 3500, -3500, TIP - 5, -(TIP - 5)):
check("C4", f"bottom at X={x:+.0f}", bottom_z(x), z_at(x, False))
for a, b in zip(EDGES, EDGES[1:]):
x = (a + b) / 2
check("C4", f"top step at X={x:+.0f}", top_z(x), z_at(x, True))
bb = bbox(cp)
check("C4", "coping Zbot", ZB, bb.Min.Z)
check("C4", "coping Ztop (highest step)", max(STEPS), bb.Max.Z)
# side width at X = 0 (side taper)
ok, curves, pts = rg.Intersect.Intersection.BrepPlane(cp, rg.Plane(rg.Point3d(0, 0, 0), rg.Vector3d.XAxis), 0.01)
ss = rg.Curve.JoinCurves(curves, 0.1)
for zz in (ZB + 1, (ZB + ZT) / 2, ZT - 1, ZT + 500):
hits = sorted(ev.PointA.Y for c in ss for ev in rg.Intersect.Intersection.CurveCurve(
c, rg.LineCurve(rg.Point3d(0, -9000, zz), rg.Point3d(0, 9000, zz)), 0.01, 0.01))
w = WB + (W - WB) * min(zz - ZB, ZT - ZB) / (ZT - ZB)
check("C4", f"side width at Z={zz:.0f}", 2 * w, (hits[-1] - hits[0]) if hits else 0)
# skewed end faces: points 3 mm inside / outside the sheared ends at y = +-(W - 50), z mid of the end face
for sx in (-1, 1):
for y in (-(W - 50), W - 50):
zz = (ZT + top_z(sx * (TIP - 10))) / 2
xe = sx * TIP - K * y
ins = cp.IsPointInside(rg.Point3d(xe - sx * 3, y, zz), 0.01, False)
out = not cp.IsPointInside(rg.Point3d(xe + sx * 3, y, zz), 0.01, False)
flag("C4", f"skewed end {'left' if sx < 0 else 'right'} at y={y:+.0f} [작도 skew {C['skew_deg']}]", ins and out,
f"x_end {xe:.1f}")
# C3 joints / overlaps
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 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)
check("C3", "overlap LC-piles (mm3)", 0, sum(inter_vol(lc, b) for b in piles), tol=1.0)
if PI:
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)
# C6 volumes
fv = F["dx"] * F["dy"] * F["thickness"] / 1e9
check("C6", "footing volume m3", fv, vol(ft) / 1e9, tol=fv * 0.001)
lv = (LN["dx"] * LN["dy"] - (len(piles) * math.pi * (PI["d"] / 2) ** 2 if PI else 0)) * LN["thickness"] / 1e9
check("C6", "lean volume m3 (minus pile holes)", lv, vol(lc) / 1e9, tol=lv * 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)
cpv = coping_volume_expected() / 1e9
check("C6", "coping volume m3 (dimension outline, shear keeps volume)", cpv, vol(cp) / 1e9, tol=cpv * 0.001)
# C7 shared pedestals and pads
S = G["seats"]
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", len(seats) == S["n_pedestals"] and sorted(pads) == sorted(int(k) for k in S["table"]),
f"pedestals={len(seats)} pads={len(pads)}")
flag("C7", "IsSolid pedestals+pads", all(b.IsSolid for b in list(seats.values()) + list(pads.values())))
for no_s, row in S["table"].items():
no = int(no_s)
elx, h1, h2, a, b = row
x, y = S["pos"][no_s]
p = pads[no]
top = top_z(x + K * y) # back to the Y = 0 step of this bearing line
check("C7", f"NO.{no} table EL.'X' - h1 - h2 = coping step", top, elx - h1 - h2)
check("C7", f"NO.{no} pad top = EL.'X'", elx, bbox(p).Max.Z)
pb = bbox(p) # plan centre: the sloped pad bottom shifts the volume centroid
check("C7", f"NO.{no} pad centre X (bbox)", x, (pb.Min.X + pb.Max.X) / 2)
check("C7", f"NO.{no} pad centre Y (bbox)", y, (pb.Min.Y + pb.Max.Y) / 2)
ped = [s for s in seats.values() if s.IsPointInside(rg.Point3d(x, y, top + 1), 0.01, False)]
flag("C7", f"NO.{no} on one pedestal", len(ped) == 1, f"{len(ped)}")
if ped:
line = rg.LineCurve(rg.Point3d(x, y, top - 10), rg.Point3d(x, y, top + 1000))
_ok, _crv, hits = rg.Intersect.Intersection.CurveBrep(line, ped[0], 0.01)
zs = [pt.Z for pt in hits]
check("C7", f"NO.{no} pedestal top at the bearing = step + h1", top + h1, max(zs) if zs else 0)
check("C7", f"NO.{no} pedestal bottom at the bearing = step", top, min(zs) if zs else 0)
check("C7", f"NO.{no} overlap pad-pedestal (mm3)", 0, inter_vol(p, ped[0]), tol=1.0)
for no, s in seats.items():
check("C7", f"pedestal {no} overlap with coping (mm3)", 0, inter_vol(s, cp), tol=1.0)
write_result(RESULT_PATH, results, dict(volumes_m3={"lean": round(vol(lc) / 1e9, 3), "footing": round(vol(ft) / 1e9, 3),
"column": round(vol(cl) / 1e9, 3), "coping": round(vol(cp) / 1e9, 3),
"pile_each": round(vol(piles[0]) / 1e9, 3) if piles else 0}))
main()
@@ -0,0 +1,264 @@
"""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()
@@ -0,0 +1,242 @@
"""G1 gate checker: concrete, piles, pedestals and pads (checks C1-C7) inside Rhino 8.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Independent expected values from EXPECTED_PATH
section G1 (drawing texts / DXF); generator params are not read.
Ported from Code/교각모델링/verify_rhino.py (same checks and order; fingerprint part moved to the gate runner).
"""
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_MM, G = X["pier_id"], X["tol_mm"], X["G1"]
results = []
f, c_, cl_, pl_ = G["footing"], G["coping"], G["column"], G["piles"]
FT_TOP = f["z0"] + f["thickness"]
EXPECTED = {
"footing": dict(dx=f["dx"], dy=f["dy"], z0=f["z0"], z1=FT_TOP),
"lean": dict(dx=f["dx"] + 2 * G["lean"]["margin"], dy=f["dy"] + 2 * G["lean"]["margin"],
z0=f["z0"] - G["lean"]["thickness"], z1=f["z0"]),
"column": dict(d=cl_["d"], z0=FT_TOP, z1=FT_TOP + cl_["height"]),
"coping": dict(dx=c_["dx"], dy=c_["dy"], z_bottom=c_["z_top_left"] - c_["rise_upper"] - c_["rise_lower"],
z_tip_bottom=c_["z_top_left"] - c_["rise_upper"], z_top_left=c_["z_top_left"],
z_top_right=c_["z_top_right"], z_top_center=c_["z_top_center"],
flat_half=c_["dx"] / 2 - c_["flat_from_tip"], top_half=c_["dx"] / 2 - c_["end_inset"],
radius=c_["radius"]),
"piles": dict(nx=pl_["nx"], ny=pl_["ny"], pitch_x=pl_.get("pitch_x", pl_.get("pitch")),
pitch_y=pl_.get("pitch_y", pl_.get("pitch")), edge=pl_["edge"], d=pl_["d"], length=pl_["length"]),
}
_a = G["dxf_arc_centres"]
DXF_LEFT_ARC_CENTER = (_a["left"][0] - _a["datum_x"], _a["datum_el"] + (_a["left"][1] - _a["datum_y"]))
DXF_RIGHT_ARC_CENTER = (_a["right"][0] - _a["datum_x"], _a["datum_el"] + (_a["right"][1] - _a["datum_y"]))
S = G["seats"]
TABLE = {int(k): tuple(v) for k, v in S["table"].items()}
NG = S["girders"]
STEP_TOP = {int(k): v for k, v in S["step_top"].items()}
STEP_X = {int(k): tuple(v) for k, v in S["step_x"].items()}
ROW_Y = S["row_y"]
PITCH = S["pitch_chain"][1] / S["pitch_chain"][0]
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_MM):
ok = abs(expected - actual) <= 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 ok else "FAIL"))
def flag(cid, name, ok, detail):
results.append(dict(check=cid, item=name, detail=detail, result="PASS" if ok else "FAIL"))
def check_seats(cp):
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")}
nos = sorted(TABLE)
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)}/{2 * len(nos)}")
m0, m1 = S["pad_margin"]
pa, pc = S["pedestal_plan"]
half_w = c_["dy"] / 2
pad_x = {}
for no, (elx, h1, h2, a, b) in TABLE.items():
g = (no - 1) % NG + 1
row = "front" if no <= NG else "rear"
s, p = seats[no], pads[no]
sb, pb = bbox(s), bbox(p)
check("C7", f"NO.{no} pedestal bottom = step {g}", STEP_TOP[g], sb.Min.Z)
check("C7", f"NO.{no} pedestal top = step + h1", STEP_TOP[g] + h1, sb.Max.Z)
check("C7", f"NO.{no} pad top = EL.'X'", elx, pb.Max.Z)
flag("C7", f"NO.{no} pedestal within step {g} / coping width",
STEP_X[g][0] - TOL_MM <= sb.Min.X and sb.Max.X <= STEP_X[g][1] + TOL_MM
and -half_w - TOL_MM <= sb.Min.Y and sb.Max.Y <= half_w + TOL_MM,
f"x {sb.Min.X:.1f}..{sb.Max.X:.1f} y {sb.Min.Y:.1f}..{sb.Max.Y:.1f}")
area = pa / 1000 * pc / 1000
check("C7", f"NO.{no} pedestal half area (m2)", area, vol(s) / h1 / 1e6, tol=area * 0.002)
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)
c = rg.VolumeMassProperties.Compute(p).Centroid
check("C7", f"NO.{no} pad centre Y (S1)", ROW_Y[row], c.Y, tol=2.0)
pad_x.setdefault(row, {})[g] = c.X
check("C7", f"NO.{no} overlap pedestal-coping (mm3)", 0,
sum(vol(x) for x in (rg.Brep.CreateBooleanIntersection(s, cp, 0.01) or [])), tol=1.0)
check("C7", f"NO.{no} overlap pad-pedestal (mm3)", 0,
sum(vol(x) for x in (rg.Brep.CreateBooleanIntersection(p, s, 0.01) or [])), tol=1.0)
for row, xs in pad_x.items():
for g in range(1, NG):
check("C7", f"{row} pad pitch girder {g}-{g + 1}", PITCH, xs[g + 1] - xs[g], tol=1.0)
first = S["rear_first_from_tip"]
for g in range(1, NG + 1):
check("C7", f"rear pad centre X girder {g} (dim {first / 1000:.3f}+{PITCH / 1000:.3f}n)",
-c_["dx"] / 2 + first + (g - 1) * PITCH, pad_x["rear"][g], tol=2.0)
for row, rng in (("front", range(1, NG + 1)), ("rear", range(NG + 1, 2 * NG + 1))):
cxs = [rg.VolumeMassProperties.Compute(seats[n]).Centroid.X for n in rng]
for g in range(NG - 1):
check("C7", f"{row} pedestal pitch girder {g + 1}-{g + 2}", PITCH, cxs[g + 1] - cxs[g], tol=1.0)
def main():
ids = {k: f"{PID}-{k}" for k in ("LC", "FT", "CL", "CP", "PL")}
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"])]
e = EXPECTED
p = e["piles"]
counts = {ids[k]: len(objs(ids[k])) for k in ("LC", "FT", "CL", "CP", "PL")}
exp_counts = {ids["LC"]: 1, ids["FT"]: 1, ids["CL"]: 1, ids["CP"]: 1, ids["PL"]: p["nx"] * p["ny"]}
flag("C5", "object counts", counts == exp_counts, str(counts))
all_b = [lc, ft, cl, cp] + piles
flag("C2", f"IsSolid all {len(all_b)}", all(b.IsSolid for b in all_b), f"{sum(b.IsSolid for b in all_b)}/{len(all_b)}")
for key, b in (("footing", ft), ("lean", lc)):
bb = bbox(b)
check("C1", f"{key} X", e[key]["dx"], bb.Max.X - bb.Min.X)
check("C1", f"{key} Y", e[key]["dy"], bb.Max.Y - bb.Min.Y)
check("C1", f"{key} Zbot", e[key]["z0"], bb.Min.Z)
check("C1", f"{key} Ztop", e[key]["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", e["column"]["d"], bb.Max.X - bb.Min.X)
check("C1", "column diameter Y", e["column"]["d"], bb.Max.Y - bb.Min.Y)
check("C1", "column Zbot", e["column"]["z0"], bb.Min.Z)
check("C1", "column Ztop", e["column"]["z1"], bb.Max.Z)
bb = bbox(cp)
check("C1", "coping length X", e["coping"]["dx"], bb.Max.X - bb.Min.X)
check("C1", "coping width Y", e["coping"]["dy"], bb.Max.Y - bb.Min.Y)
check("C1", "coping Zbot", e["coping"]["z_bottom"], bb.Min.Z)
check("C1", "coping Ztop (right end)", e["coping"]["z_top_right"], bb.Max.Z)
xs = sorted({round((bbox(b).Min.X + bbox(b).Max.X) / 2, 1) for b in piles})
ys = sorted({round((bbox(b).Min.Y + bbox(b).Max.Y) / 2, 1) for b in piles})
flag("C1", "pile grid", len(xs) == p["nx"] and len(ys) == p["ny"], f"x={xs} y={ys}")
check("C1", "pile edge X", p["edge"], xs[0] - (-e["footing"]["dx"] / 2))
check("C1", "pile edge Y", p["edge"], ys[0] - (-e["footing"]["dy"] / 2))
for a, b2 in zip(xs, xs[1:]):
check("C1", "pile pitch X", p["pitch_x"], b2 - a)
for a, b2 in zip(ys, ys[1:]):
check("C1", "pile pitch Y", p["pitch_y"], b2 - a)
pb = bbox(piles[0])
check("C1", "pile diameter", p["d"], pb.Max.X - pb.Min.X)
check("C1", "pile length", p["length"], pb.Max.Z - pb.Min.Z)
check("C1", "pile head embedment", pl_["head_embedment"], pb.Max.Z - e["footing"]["z0"])
# C4 coping front section at Y=0 (re-projection to drawing)
plane = rg.Plane(rg.Point3d(0, 0, 0), rg.Vector3d.YAxis)
ok, curves, pts = rg.Intersect.Intersection.BrepPlane(cp, plane, 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)")
c = e["coping"]
def z_at(x, upper):
line = rg.LineCurve(rg.Point3d(x, 0, 0), rg.Point3d(x, 0, 60000))
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]
sx = {k: (v[0] + v[1]) / 2 for k, v in STEP_X.items()} # sample each top step at its middle
check("C4", "bottom flat at X=0", c["z_bottom"], z_at(0, False))
check("C4", f"bottom flat at X=+{c['flat_half'] - 1:.0f}", c["z_bottom"], z_at(c["flat_half"] - 1, False))
check("C4", f"bottom at X=-{c['top_half'] + 199:.0f} (tip)", c["z_tip_bottom"], z_at(-c["top_half"] - 199, False), tol=2.0)
check("C4", f"top left step (X={sx[1]:.0f})", c["z_top_left"], z_at(sx[1], True))
check("C4", "top centre step (X=0)", c["z_top_center"], z_at(0, True))
check("C4", f"top right step (X={sx[NG]:.0f})", c["z_top_right"], z_at(sx[NG], True))
worst = 0.0
step = (c["dx"] / 2 - c["flat_half"] - 200) / 50
for side, (cx, cz) in ((1, DXF_RIGHT_ARC_CENTER), (-1, DXF_LEFT_ARC_CENTER)):
for k in range(1, 50):
x = side * (c["flat_half"] + 100 + k * step)
zb = z_at(x, False)
worst = max(worst, abs(math.hypot(x - cx, zb - cz) - c["radius"]))
check("C4", "soffit radius max deviation vs DXF ARC centre (98 samples)", 0, worst)
# 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)
check("C3", "overlap LC-piles (mm3)", 0, sum(inter_vol(lc, b) for b in piles), tol=1.0)
emb = len(piles) * math.pi * (p["d"] / 2) ** 2 * pl_["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 flat zone", e["column"]["d"] / 2 <= c["flat_half"]
and e["column"]["d"] / 2 <= c["dy"] / 2,
f"r={e['column']['d'] / 2:.0f} <= {c['flat_half']:.0f} (X), {c['dy'] / 2:.0f} (Y)")
# C6 volumes vs analytic formulas
fv = f["dx"] / 1000 * f["dy"] / 1000 * f["thickness"] / 1000
check("C6", "footing volume m3", fv, vol(ft) / 1e9, tol=fv * 0.001)
cv_ = math.pi * (cl_["d"] / 2000) ** 2 * cl_["height"] / 1000
check("C6", "column volume m3", cv_, vol(cl) / 1e9, tol=round(cv_, 2) * 0.001)
lv = (e["lean"]["dx"] / 1000 * e["lean"]["dy"] / 1000 * G["lean"]["thickness"] / 1000
- len(piles) * math.pi * (p["d"] / 2000) ** 2 * G["lean"]["thickness"] / 1000)
check("C6", "lean volume m3", lv, vol(lc) / 1e9, tol=lv * 0.001)
poly = [tuple(q) for q in G["coping_outline"]]
area = 0.5 * abs(sum(x1 * y2 - x2 * y1 for (x1, y1), (x2, y2) in zip(poly, poly[1:] + poly[:1])))
(x1, z1), (x2, z2) = poly[1], poly[2]
chord = math.hypot(x2 - x1, z2 - z1)
theta = 2 * math.asin(chord / 2 / c["radius"])
seg = c["radius"] ** 2 / 2 * (theta - math.sin(theta))
cv = (area - 2 * seg) * c["dy"] / 1e9
check("C6", "coping volume m3", cv, vol(cp) / 1e9, tol=cv * 0.001)
if objs(f"{PID}-BS") or objs(f"{PID}-BP"):
check_seats(cp)
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))},
pile_volume_each_m3=round(vol(piles[0]) / 1e9, 3)))
main()
@@ -0,0 +1,257 @@
"""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()
@@ -0,0 +1,143 @@
"""G2 gate checker: column rebar P1-P6 (check C8) inside Rhino 8.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values from EXPECTED_PATH sections G1 (footing
bottom) and G2 (D3 schedule L / counts, D1 dimension chains, section radii); generator params are not read.
Ported from Code/교각모델링/verify_rebar.py (same checks and order).
"""
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, chain, load_expected, write_result # noqa: E402
doc = __rhino_doc__
X = load_expected(EXPECTED_PATH)
PID, TOL, G = X["pier_id"], X["tol_mm"], X["G2"]
SCHED_L, SCHED_N, DIA = G["schedule_L"], G["schedule_n"], G["dia"]
FB = X["G1"]["footing"]["z0"]
LEVELS = chain(G["hoop_chain"], FB)
TIE_LEVELS = chain(G["tie_chain"], FB)
R_OUT, R_IN, R_HOOP = G["r_outer"], G["r_inner"], G["r_hoop"]
TOP = FB + sum(G["p1_top_parts"])
NDIV = G["main_divisions"]
results = []
def check(item, exp, act, tol=TOL):
results.append(dict(check="C8", 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="C8", item=item, detail=detail, result="PASS" if ok else "FAIL"))
def ids(prefix):
return [o for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "").startswith(prefix)]
def concrete():
out = []
for oid in (f"{PID}-CL", f"{PID}-FT", f"{PID}-CP"):
o = [x for x in all_objects(doc) if x.Attributes.GetUserString("id") == oid][0]
out.append(o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry))
return out
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}
for m in SCHED_L:
occ = {o.Attributes.GetUserString("occurrence").rstrip("ab") for o in cl[m]}
check(f"{m} bar count", SCHED_N[m], len(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"pipes {len(pipes[m])} / centrelines {len(cl[m])}")
for m in SCHED_L:
per = {}
for o in cl[m]:
k = o.Attributes.GetUserString("occurrence").rstrip("ab")
per[k] = per.get(k, 0) + float(o.Attributes.GetUserString("design_length")) + float(
o.Attributes.GetUserString("coupler_length") or 0) # schedule L includes coupler b (D3 V, IX)
worst = max(abs(v - SCHED_L[m]) for v in per.values())
check(f"{m} original length (+coupler b) vs schedule L (worst of {len(per)})", 0, worst)
curve = {}
for o in cl[m]:
k = o.Attributes.GetUserString("occurrence").rstrip("ab")
curve[k] = curve.get(k, 0) + o.Geometry.GetLength() + float(
o.Attributes.GetUserString("coupler_length") or 0)
dev = [v - SCHED_L[m] for v in curve.values()]
results.append(dict(check="C8", item=f"{m} curve length - L (record, fillets shorten hooks)",
detail=f"min {min(dev):.1f} / max {max(dev):.1f} mm", result="INFO"))
if m in G["no_fillet_marks"]:
check(f"{m} curve length (+coupler b) vs L (no fillet)", 0, max(abs(d) for d in dev))
for m, r in (("P1", R_OUT), ("P2", R_IN)):
angs, worst_r = [], 0
for o in cl[m]:
c = o.Geometry
p = c.PointAtEnd if c.PointAtEnd.Z > c.PointAtStart.Z else c.PointAtStart
angs.append(math.degrees(math.atan2(p.Y, p.X)) % 360)
worst_r = max(worst_r, abs(math.hypot(p.X, p.Y) - r))
check(f"{m} top EL", TOP, p.Z) if o is cl[m][0] else None
angs.sort()
worst_a = max(abs(a - i * 360 / NDIV) for i, a in enumerate(angs))
check(f"{m} radius (worst)", 0, worst_r, tol=0.5)
check(f"{m} angle vs i*360/{NDIV} (worst, deg)", 0, worst_a, tol=0.01)
p1b = min(min(o.Geometry.PointAtStart.Z, o.Geometry.PointAtEnd.Z) for o in cl["P1"])
p2b = min(min(o.Geometry.PointAtStart.Z, o.Geometry.PointAtEnd.Z) for o in cl["P2"])
dz = G["bottom_above_footing"]
check(f"P1 hook leg EL (footing bottom + {dz})", FB + dz, p1b)
check("P2 bottom EL", FB + dz, p2b)
hz = {}
for m in ("P3", "P4", "P5"):
for o in cl[m]:
bb = o.Geometry.GetBoundingBox(True)
hz.setdefault(round(bb.Max.Z, 1), set()).add(m)
if m != "P3":
p = o.Geometry.PointAtStart
check(f"{m} radius", R_HOOP, math.hypot(p.X, p.Y), tol=0.5) if o is cl[m][0] else None
zs = sorted(hz)
flag(f"hoop level count {len(LEVELS)}", len(zs) == len(LEVELS), f"{len(zs)}")
check("hoop levels vs dim chain (worst)", 0, max(abs(a - b) for a, b in zip(zs, LEVELS)))
marks_by_level = [list(hz[z])[0] for z in zs]
zones = G["hoop_marks_by_zone"]
flag("hoop marks by zone (" + ", ".join(f"{m} x{n}" for m, n in zones) + ")",
marks_by_level == [m for m, n in zones for _ in range(n)], "")
tz = sorted({round(o.Geometry.GetBoundingBox(True).Max.Z, 1) for o in cl["P6"]})
off = G["tie_offset"]
check(f"tie levels vs chain + {off} (worst)", 0, max(abs(a - (b + off)) for a, b in zip(tz, TIE_LEVELS)))
solids = concrete()
union = rg.Brep.CreateBooleanUnion(solids, 0.01)
shell = union[0] if union and len(union) == 1 else None # outer surface only (no internal joint faces)
results.append(dict(check="C8", item="concrete union for cover", detail=f"{len(union) if union else 0} brep",
result="INFO"))
min_cover = {}
for m in SCHED_L:
bad, cov = 0, 1e9
for o in cl[m]:
c = o.Geometry
n = max(4, int(c.GetLength() / 100))
for t in c.DivideByCount(n, 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 * (DIA[m] / 2), p - radial * (DIA[m] / 2),
p + rg.Vector3d(0, 0, DIA[m] / 2), p - rg.Vector3d(0, 0, DIA[m] / 2)):
if not any(s.IsPointInside(q, 0.01, False) for s in solids):
bad += 1
d = p.DistanceTo(shell.ClosestPoint(p)) if shell and shell.IsPointInside(p, 0.01, False) else -1
cov = min(cov, d - DIA[m] / 2)
min_cover[m] = round(cov, 1)
check(f"{m} sample points outside concrete", 0, bad, tol=0)
results.append(dict(check="C8", item="min cover to concrete surface per mark (record)",
detail=json.dumps(min_cover), result="INFO"))
results.append(dict(check="C8", item="clearance P1 outer surface vs hoop inner surface (record)",
detail=f"{R_HOOP - DIA['P4'] / 2 - (R_OUT + DIA['P1'] / 2):.1f} mm", result="INFO"))
write_result(RESULT_PATH, results, dict(min_cover=min_cover))
main()
@@ -0,0 +1,156 @@
"""G2 gate checker, form "ring" (Ulsan P1, 2026-10-13): column rebar with one main ring, hoop zones, cross ties.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G2 + G1 footing / column) come
from the schedule (L, counts, R) and the dimension chains of the rebar drawing; generator params are not read.
Checks (C8): counts, original length (+coupler) vs schedule L, curve length for bars without fillets, main bar
radius / division / leg and top level, hoop level chain / zones / radius, tie levels / count per level,
containment in concrete and minimum 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, chain, load_expected, write_result # noqa: E402
doc = __rhino_doc__
X = load_expected(EXPECTED_PATH)
PID, TOL, G = X["pier_id"], X["tol_mm"], X["G2"]
SCHED_L, SCHED_N, DIA = G["schedule_L"], G["schedule_n"], G["dia"]
FB = X["G1"]["footing"]["z0"]
M = G["main"]
LEVELS = chain(G["hoop_chain"], FB)
TIE = G["tie"]
TIE_LEVELS = chain(TIE["chain"], FB)
results = []
def check(item, exp, act, tol=TOL):
results.append(dict(check="C8", 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="C8", item=item, detail=detail, result="PASS" if ok else "FAIL"))
def info(item, detail):
results.append(dict(check="C8", 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")
tie_mark = TIE["mark"]
for m in SCHED_L:
n_occ = len(cl[m]) if m == tie_mark else len({occ(o) for o in 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 = o.Attributes.GetUserString("occurrence") if m == tie_mark else occ(o)
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 not per:
continue
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 - L (record)", f"min {min(dev):.1f} / max {max(dev):.1f} mm")
if m in G["no_fillet_marks"]:
check(f"{m} curve length (+coupler) vs L (no fillet)", 0, max(abs(d) for d in dev))
# main bars
mm_, R0, ND = M["mark"], M["radius"], M["divisions"]
angs, worst_r, tops, legs = [], 0, [], []
for o in cl[mm_]:
c = o.Geometry
p = c.PointAtEnd if c.PointAtEnd.Z > c.PointAtStart.Z else c.PointAtStart
angs.append(math.degrees(math.atan2(p.Y, p.X)) % 360)
worst_r = max(worst_r, abs(math.hypot(p.X, p.Y) - R0))
tops.append(p.Z)
legs.append(min(c.PointAtStart.Z, c.PointAtEnd.Z))
angs.sort()
check(f"{mm_} radius (worst)", 0, worst_r, tol=0.5)
check(f"{mm_} angle vs start + i*360/{ND} (worst, deg)", 0,
max(abs(a - (M.get("start_deg", 0) + i * 360 / ND)) for i, a in enumerate(angs)), tol=0.01)
check(f"{mm_} top EL (worst)", 0, max(abs(t - (FB + sum(M["top_parts"]))) for t in tops))
check(f"{mm_} hook leg EL (footing bottom + {M['bottom_above_footing']}) (worst)", 0,
max(abs(z - (FB + M["bottom_above_footing"])) for z in legs))
# hoops
hz = {}
for m, _ in G["hoop_zones"]:
for o in cl[m]:
hz.setdefault(round(o.Geometry.GetBoundingBox(True).Max.Z, 1), set()).add(m)
zs = sorted(hz)
flag(f"hoop level count {len(LEVELS)}", len(zs) == len(LEVELS), f"{len(zs)}")
check("hoop levels vs dim chain (worst)", 0, max(abs(a - b) for a, b in zip(zs, LEVELS)) if zs else 1e9)
flag("hoop marks by zone (" + ", ".join(f"{m} x{n}" for m, n in G["hoop_zones"]) + ")",
[list(hz[z])[0] for z in zs] == [m for m, n in G["hoop_zones"] for _ in range(n)])
for m, _ in G["hoop_zones"]:
o = cl[m][0]
p = o.Geometry.PointAt(o.Geometry.Domain.Mid)
check(f"{m} radius (mid point)", G.get("r_hoop_by_mark", {}).get(m, G["r_hoop"]), math.hypot(p.X, p.Y), tol=0.5)
if "inner" in M: # Okseong P1: inner ring (2026-10-14)
im = M["inner"]
tops = [max((o.Geometry.PointAtStart, o.Geometry.PointAtEnd), key=lambda q: q.Z) for o in cl[im["mark"]]]
lows = [min((o.Geometry.PointAtStart, o.Geometry.PointAtEnd), key=lambda q: q.Z) for o in cl[im["mark"]]]
check(f"{im['mark']} inner ring radius (worst)", 0, max(abs(math.hypot(q.X, q.Y) - im["radius"]) for q in tops) if tops else 1e9, tol=0.5)
flag(f"{im['mark']} hooks bent inwards", bool(lows) and all(math.hypot(q.X, q.Y) < im["radius"] for q in lows))
# ties
if TIE.get("y_lift"): # Okseong P1: X tie at the level, Y tie lifted
occ_ = lambda o: o.Attributes.GetUserString("occurrence")
lx = sorted(round(o.Geometry.GetBoundingBox(True).Max.Z, 1) for o in cl[tie_mark] if occ_(o).endswith("X"))
ly = sorted(round(o.Geometry.GetBoundingBox(True).Max.Z, 1) for o in cl[tie_mark] if occ_(o).endswith("Y"))
flag(f"tie X / Y levels {len(TIE_LEVELS)} each", len(lx) == len(ly) == len(TIE_LEVELS), f"X {len(lx)} Y {len(ly)}")
check("tie X levels vs chain (worst)", 0, max(abs(a - b - TIE["offset"]) for a, b in zip(lx, TIE_LEVELS)) if lx else 1e9)
check(f"tie Y = X + {TIE['y_lift']} (worst)", 0, max(abs(b - a - TIE["y_lift"]) for a, b in zip(lx, ly)) if ly else 1e9)
else:
tz = {}
for o in cl[tie_mark]:
tz.setdefault(round(o.Geometry.GetBoundingBox(True).Max.Z, 1), []).append(o)
lv = sorted(tz)
flag(f"tie levels {len(TIE_LEVELS)} x {TIE['per_level']}", len(lv) == len(TIE_LEVELS)
and all(len(v) == TIE["per_level"] for v in tz.values()), f"{len(lv)} levels")
check(f"tie levels vs chain + {TIE['offset']} (worst)", 0,
max(abs(a - (b + TIE["offset"])) for a, b in zip(lv, TIE_LEVELS)) if lv else 1e9)
# containment and cover
solids = []
for oid in (f"{PID}-CL", f"{PID}-FT", f"{PID}-CP"):
o = [x for x in all_objects(doc) if x.Attributes.GetUserString("id") == oid][0]
solids.append(o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry))
union = rg.Brep.CreateBooleanUnion(solids, 0.01)
shell = union[0] if union and len(union) == 1 else None
info("concrete union for cover", f"{len(union) if union else 0} brep")
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
d = p.DistanceTo(shell.ClosestPoint(p)) if shell and shell.IsPointInside(p, 0.01, False) else -1
cov = min(cov, d - r)
min_cover[m] = round(cov, 1)
check(f"{m} sample points outside concrete", 0, bad, tol=0)
info("min cover to concrete surface per mark (record)", json.dumps(min_cover))
write_result(RESULT_PATH, results, dict(min_cover=min_cover))
main()
@@ -0,0 +1,163 @@
"""G2 gate checker, form "straight" (Ramp-A P1, 2026-10-13): straight main ring with a bottom coupler, lapped
half-ring hoops, X / Y cross ties; column on a drilled shaft (no footing).
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G2) come from the schedule (L, N, r)
and the D2 dimension chains (anchor EL 45.492: '2,000' below, 14@150 / 78@150 hoops, 7@300 / 39@300 ties) with
the user decisions K1-K3; generator params are not read.
Checks (C8): counts, original length (+coupler) vs schedule L, main radius / division / bottom / top, hoop and tie
level grids and radius, X / Y ties apart, containment in concrete (pile segments, column, coping), min 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["G2"]
SCHED_L, SCHED_N, DIA = G["schedule_L"], G["schedule_n"], G["dia"]
M, HP, TI = G["main"], G["hoops"], G["ties"]
A = G["chain_anchor"]
def grid(g):
return [A - g["below"] * g["pitch"] + k * g["pitch"] for k in range(SCHED_N[g["mark"]] // g.get("per_level", 1))]
HOOP_LEVELS, TIE_LEVELS = grid(HP), grid(TI)
CO = X["G1"]["column"]
R_COL, Z_CL0, Z_CL1 = CO["d"] / 2, CO["z0"], CO["z0"] + CO["length"]
results = []
def check(item, exp, act, tol=TOL):
results.append(dict(check="C8", 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="C8", item=item, detail=detail, result="PASS" if ok else "FAIL"))
def info(item, detail):
results.append(dict(check="C8", 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")
tie = TI["mark"]
for m in SCHED_L:
n_occ = len(cl[m]) if m == tie else len({occ(o) for o in 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 = o.Attributes.GetUserString("occurrence") if m == tie else occ(o)
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: straight, chain bottom (anchor - 2,000), top = bottom + L - coupler (K1)
mm_ = M["mark"]
bottom = A - M["below_anchor"]
top = bottom + SCHED_L[mm_] - M["coupler"]
angs, worst_r, zb, zt = [], 0, [], []
for o in cl[mm_]:
c = o.Geometry
lo, hi = sorted((c.PointAtStart, c.PointAtEnd), key=lambda p: p.Z)
angs.append(math.degrees(math.atan2(hi.Y, hi.X)) % 360)
worst_r = max(worst_r, abs(math.hypot(lo.X, lo.Y) - M["radius"]), abs(math.hypot(hi.X, hi.Y) - M["radius"]))
zb.append(lo.Z)
zt.append(hi.Z)
angs.sort()
check(f"{mm_} radius (worst)", 0, worst_r, tol=0.5)
check(f"{mm_} angle vs {M['start_deg']} + i*360/{M['divisions']} (worst, deg)", 0,
max(abs(a - (M["start_deg"] + i * 360 / M["divisions"])) for i, a in enumerate(angs)), tol=0.01)
check(f"{mm_} bottom EL = chain {A / 1000:.3f} - {M['below_anchor']} (worst)", 0, max(abs(z - bottom) for z in zb))
check(f"{mm_} top EL = bottom + L - coupler {M['coupler']} (worst)", 0, max(abs(z - top) for z in zt))
# hoops: levels on the chain grid, two pieces per level, radius
hz = {}
for o in cl[HP["mark"]]:
bb = o.Geometry.GetBoundingBox(True)
hz.setdefault(round((bb.Max.Z + bb.Min.Z) / 2, 1), []).append(o)
zs = sorted(hz)
flag(f"{HP['mark']} 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"{HP['mark']} levels vs chain grid {HP['pitch']} (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[HP["mark"]][:4]:
c = o.Geometry
p = c.PointAt(c.Domain.Mid)
check(f"{HP['mark']} radius at mid point ({o.Attributes.GetUserString('occurrence')})", HP["radius"],
math.hypot(p.X, p.Y), tol=0.5)
# ties: X at the grid level, Y lifted (K3); levels = top of the straight part
by = lambda s: [o.Geometry for o in cl[tie] if o.Attributes.GetUserString("occurrence").endswith(s)]
xs, ys = by("X"), by("Y")
txm = sorted(round(g.GetBoundingBox(True).Max.Z, 1) for g in xs)
tym = sorted(round(g.GetBoundingBox(True).Max.Z, 1) for g in ys)
flag(f"{tie} 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):
check(f"{tie} X levels vs chain grid {TI['pitch']} (worst)", 0, max(abs(a - b) for a, b in zip(txm, TIE_LEVELS)))
if len(tym) == len(TIE_LEVELS):
check(f"{tie} Y levels = X + {TI['y_lift']} (worst)", 0, max(abs(a - b - TI["y_lift"]) for a, b in zip(tym, TIE_LEVELS)))
xs.sort(key=lambda g: g.GetBoundingBox(True).Max.Z)
ys.sort(key=lambda g: g.GetBoundingBox(True).Max.Z)
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"{tie} X / Y centrelines apart >= bar diameter {DIA[tie]} (min, capped)", DIA[tie], min(gap, DIA[tie]), tol=0.01)
info(f"{tie} X / Y centreline distance (min, record)", f"{gap:.2f} mm")
# containment and cover
solids = []
for o in all_objects(doc):
oid = o.Attributes.GetUserString("id") or ""
if oid in (f"{PID}-CL", f"{PID}-CP") or oid.startswith(f"{PID}-PL"):
solids.append(o.Geometry if isinstance(o.Geometry, rg.Brep) else rg.Brep.TryConvertBrep(o.Geometry))
union = rg.Brep.CreateBooleanUnion(solids, 0.01)
shell = union[0] if union and len(union) == 1 else None
info("concrete union for cover", f"{len(union) if union else 0} brep from {len(solids)} solids"
+ ("" if shell else " -> min cover = column side cover in the column zone (R - radial distance - bar radius)"))
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
if shell:
d = p.DistanceTo(shell.ClosestPoint(p)) if shell.IsPointInside(p, 0.01, False) else -1
cov = min(cov, d - r)
elif Z_CL0 <= p.Z <= Z_CL1: # no union (coplanar contact faces): side cover in the column zone
cov = min(cov, R_COL - math.hypot(p.X, p.Y) - r)
min_cover[m] = round(cov, 1)
check(f"{m} sample points outside concrete", 0, bad, tol=0)
info("min cover to concrete surface per mark (record)", json.dumps(min_cover))
write_result(RESULT_PATH, results, dict(min_cover=min_cover))
main()
@@ -0,0 +1,146 @@
"""G3 gate checker: footing rebar B1-B12 (check C9) and rebar colours inside Rhino 8.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values from EXPECTED_PATH sections G1 (footing
bottom) and G3 (D3 schedule, D1/D2 dimension chains, F1 layer heights); mid-layer positions are drawn dots
(derived, not independent). Generator params are not read.
Ported from Code/교각모델링/verify_footing_rebar.py (same checks and order).
"""
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, load_expected, row, series, 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"]
Y_BOT, Y_250, X_BOT, X_250 = (row(G["rows"][k]) for k in ("Y_BOT", "Y_250", "X_BOT", "X_250"))
Z = G["layer_z"]
B10_Z = series(G["b10_levels"])
B9_X, B9_Y = series(G["b9_grid"]["x"]), series(G["b9_grid"]["y"])
PALETTE = {int(k): tuple(v) for k, v in X["rebar_palette"].items()}
results = []
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 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]:
worst = max(abs(float(o.Attributes.GetUserString("design_length")) - L) for o in cl[m])
check(f"{m} original length vs L (worst)", 0, worst)
dev = [o.Geometry.GetLength() - L for o in cl[m]]
info(f"{m} curve length - L (bend fillets)", f"min {min(dev):.1f} / max {max(dev):.1f}")
def mids(m):
return [o.Geometry.PointAt(o.Geometry.Domain.Mid) for o in cl[m]]
for m in ("B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8"):
ps = mids(m)
check(f"{m} layer z (worst)", 0, max(abs(p.Z - (FB + Z[m])) for p in ps))
def pos_check(marks, axis, expected, name):
got = sorted(round(getattr(p, axis), 1) for m in marks for p in mids(m))
ok = len(got) == len(expected)
worst = max(abs(a - b) for a, b in zip(got, expected)) if ok else 1e9
check(f"{name} positions vs chain (worst, n={len(got)}/{len(expected)})", 0, worst)
pos_check(("B2", "B4"), "X", Y_BOT, "bottom Y-bars B2+B4")
pos_check(("B6", "B8"), "Y", X_BOT, "bottom X-bars B6+B8")
pos_check(("B3",), "X", Y_250, "B3")
pos_check(("B1",), "X", Y_250, "B1")
pos_check(("B7",), "Y", X_250, "B7")
pos_check(("B5",), "Y", X_250, "B5")
ye, xe = G["rows"]["Y_BOT"], G["rows"]["X_BOT"]
b2x = sorted(round(p.X) for p in mids("B2"))
flag(f"B2 at ends and alternate from {ye['start']}", b2x == sorted(list(ye["ends"]) + Y_BOT[1:-1][0::2]), "")
b6y = sorted(round(p.Y) for p in mids("B6"))
flag(f"B6 at ends and alternate from {xe['start']}", b6y == sorted(list(xe["ends"]) + X_BOT[1:-1][0::2]), "")
mid = G["mid_levels"]
for m in ("B11", "B12"):
zs = sorted({round(p.Z - FB) for p in mids(m)})
flag(f"{m} mid levels {'/'.join(str(v) for v in mid)} [derived]", zs == mid, str(zs))
zs = sorted(round(o.Geometry.GetBoundingBox(True).Min.Z - FB, 1) for o in cl["B10"])
check("B10 levels vs chain (worst)", 0, max(abs(a - b) for a, b in zip(zs, B10_Z)) if len(zs) == len(B10_Z) else 1e9)
bb = cl["B10"][0].Geometry.GetBoundingBox(True)
check("B10 centreline size X", G["b10_size"][0], bb.Max.X - bb.Min.X)
check("B10 centreline size Y", G["b10_size"][1], bb.Max.Y - bb.Min.Y)
# B9: vertical leg x = bbox min X (hooks point +X); y constant
pts = {(round(o.Geometry.GetBoundingBox(True).Min.X), round(o.Geometry.GetBoundingBox(True).Center.Y))
for o in cl["B9"]}
exp = {(x, y) for i, x in enumerate(B9_X) for j, y in enumerate(B9_Y) if (i + j) % 2 == 0}
flag(f"B9 positions = checkerboard of {len(B9_X)}x{len(B9_Y)} grid", pts == exp, f"{len(pts)} / {len(exp)}")
zr = [o.Geometry.GetBoundingBox(True) for o in cl["B9"]]
e0, e1 = G["b9_erec"]
a9, c9 = G["b9_a"], G["b9_c"]
check("B9 bottom z (worst of all)", 0, max(abs(b.Min.Z - (FB + (e0 + e1) / 2 - a9 / 2)) for b in zr))
check("B9 vertical extent a (bottom leg z to top of straight)", a9,
max(b.Max.Z for b in zr[:1]) - min(b.Min.Z for b in zr[:1]) - c9 / math.pi, tol=1.0)
solids = []
for oid in (f"{PID}-FT", f"{PID}-CL", f"{PID}-CP"):
o = [x for x in all_objects(doc) if x.Attributes.GetUserString("id") == oid][0]
solids.append(o.Geometry)
ft = solids[0]
shell = None
for order in ([solids[1], solids[0], solids[2]], solids): # union is order-sensitive in Rhino 8
u = rg.Brep.CreateBooleanUnion(order, 0.01)
if u and len(u) == 1:
shell = u[0]
break
if shell is None:
raise RuntimeError("concrete union failed")
cover = {}
for m in SCHED:
bad, cov = 0, 1e9
for o in cl[m]:
c = o.Geometry
for t in c.DivideByCount(max(4, int(c.GetLength() / 200)), True):
p = c.PointAt(t)
r = DIA[m] / 2
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(shell.ClosestPoint(p)) - r)
cover[m] = round(cov, 1)
check(f"{m} sample points outside footing", 0, bad, tol=0)
info("min cover to concrete exterior per mark", 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[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()
@@ -0,0 +1,158 @@
"""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()
@@ -0,0 +1,115 @@
"""G3 gate checker, footing rebar as centreline polylines (Okseong P1, 2026-10-14): mats B1~B8, mid layers B11 / B12,
perimeter loops B10, vertical ties B9.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G3) come from the schedule (L, N), the
front / side dimension chains (bar positions), the leg lengths b of the schedule, the drawn mat / loop levels and
the B9 grid rule with the user decisions F1-F4; generator params are not read.
Checks (C9): counts, original length vs L, per mat: positions vs chain, straight level, span, leg length and
direction; mid layers; B10 levels / size; B9 grid / height; containment in the footing; min 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, chain, load_expected, write_result # noqa: E402
doc = __rhino_doc__
X = load_expected(EXPECTED_PATH)
PID, G = X["pier_id"], X["G3"]
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
results = []
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 bb(o):
return o.Geometry.GetBoundingBox(True)
def worst(a, b):
return max(abs(x - y) for x, y in zip(a, b)) if len(a) == len(b) and a else 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])}")
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]))
half = G["half_a"]
for name, mt in G["mats"].items():
objs = [o for m in mt["marks"] for o in cl[m]]
axis = mt["axis"]
pos = sorted(round((bb(o).Min.X + bb(o).Max.X) / 2 if axis == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in objs)
want = chain(mt["chain"])
check(f"{name} positions vs chain ({len(pos)}/{len(want)}, worst)", 0, worst(pos, want))
leg = mt["leg"]
straight = [bb(o).Min.Z if leg > 0 else bb(o).Max.Z for o in objs]
check(f"{name} straight part level {mt['z']} (worst)", 0, max(abs(z - mt["z"]) for z in straight) if straight else 1e9)
span = [(bb(o).Max.Y - bb(o).Min.Y) if axis == "Y" else (bb(o).Max.X - bb(o).Min.X) for o in objs]
check(f"{name} span 2 x {half} (worst)", 0, max(abs(s - 2 * half) for s in span) if span else 1e9)
legs = [(bb(o).Max.Z - bb(o).Min.Z) for o in objs]
check(f"{name} leg {abs(leg)} {'up' if leg > 0 else 'down'} (worst)", 0, max(abs(h - abs(leg)) for h in legs) if legs else 1e9)
a, b = mt["marks"]
ia = sorted(round((bb(o).Min.X + bb(o).Max.X) / 2 if axis == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[a])
flag(f"{name} {a}/{b} alternate from the edge (F3)", ia == want[0::2], f"{a} {len(ia)}")
for m, md in G["mid"].items():
got = sorted((round(bb(o).Max.Z, 1), round((bb(o).Min.X + bb(o).Max.X) / 2 if md["axis"] == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1)) for o in cl[m])
want = sorted((md["z_hi"] if round(p) % 1000 == 0 else md["z_lo"], round(p, 1)) for p in chain(md["chain"]))
flag(f"{m} levels / positions ({len(want)})", len(got) == len(want) and all(abs(g[0] - w[0]) <= 1 and abs(g[1] - w[1]) <= 1 for g, w in zip(got, want)),
f"{got[:3]} vs {want[:3]}")
b10 = G["b10"]
zs = sorted(round(bb(o).Min.Z, 1) for o in cl["B10"])
check("B10 levels (worst)", 0, worst(zs, sorted(b10["levels"])))
check("B10 loop size X (worst)", 0, max(abs(bb(o).Max.X - bb(o).Min.X - b10["size"]) for o in cl["B10"]) if cl["B10"] else 1e9)
b9 = G["b9"]
cols = sorted({round((bb(o).Min.X + bb(o).Max.X) / 2, 1) for o in cl["B9"]})
flag(f"B9 columns {b9['n_cols']} @{b9['pitch_x']}", len(cols) == b9["n_cols"] and all(abs(b - a - b9["pitch_x"]) < 1 for a, b in zip(cols, cols[1:])), f"{len(cols)}")
check("B9 height top-bottom (worst)", b9["height"], max((bb(o).Max.Z - bb(o).Min.Z for o in cl["B9"]), key=lambda h: abs(h - b9["height"])) if cl["B9"] else 0)
flag("B9 hooks the top and bottom Y bars", all(bb(o).Max.Z > G["mats"]["y_top"]["z"] and bb(o).Min.Z < G["mats"]["y_bottom"]["z"] for o in cl["B9"]))
# containment and cover
ft = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-FT"][0]
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() / 300)), 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 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 the footing", 0, bad, tol=0)
info("min cover to the footing surface per mark (record)", json.dumps(cover))
write_result(RESULT_PATH, results, dict(cover=cover))
main()
@@ -0,0 +1,181 @@
"""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()
@@ -0,0 +1,124 @@
"""G3 gate checker, footing rebar as centreline polylines with drawn position lists (Saejae-1 P1, 2026-10-15; adapted from g3_poly.py,
Okseong P1): mats B1~B8, mid layers B11 / B12, perimeter loops B10, vertical ties B9 (6 rows, 8 / 7 per row alternating).
Positions are explicit lists (expected G3 "pos" / "levels" / "b9") typed from the drawn points, not dimension chains.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G3) come from the schedule (L, N), the
front / side dimension chains (bar positions), the leg lengths b of the schedule, the drawn mat / loop levels and
the B9 grid rule with the user decisions F1-F4; generator params are not read.
Checks (C9): counts, original length vs L, per mat: positions vs chain, straight level, span, leg length and
direction; mid layers; B10 levels / size; B9 grid / height; containment in the footing; min 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, G = X["pier_id"], X["G3"]
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
results = []
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 bb(o):
return o.Geometry.GetBoundingBox(True)
def mid_(o):
return (bb(o).Min.X + bb(o).Max.X) / 2, (bb(o).Min.Y + bb(o).Max.Y) / 2
def worst(a, b):
return max(abs(x - y) for x, y in zip(a, b)) if len(a) == len(b) and a else 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])}")
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]))
half = G["half_a"]
for name, mt in G["mats"].items():
objs = [o for m in mt["marks"] for o in cl[m]]
axis = mt["axis"]
pos = sorted(round((bb(o).Min.X + bb(o).Max.X) / 2 if axis == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in objs)
want = sorted(mt["pos"])
check(f"{name} positions vs chain ({len(pos)}/{len(want)}, worst)", 0, worst(pos, want))
leg = mt["leg"]
straight = [bb(o).Min.Z if leg > 0 else bb(o).Max.Z for o in objs]
check(f"{name} straight part level {mt['z']} (worst)", 0, max(abs(z - mt["z"]) for z in straight) if straight else 1e9)
span = [(bb(o).Max.Y - bb(o).Min.Y) if axis == "Y" else (bb(o).Max.X - bb(o).Min.X) for o in objs]
check(f"{name} span 2 x {half} (worst)", 0, max(abs(s - 2 * half) for s in span) if span else 1e9)
legs = [(bb(o).Max.Z - bb(o).Min.Z) for o in objs]
check(f"{name} leg {abs(leg)} {'up' if leg > 0 else 'down'} (worst)", 0, max(abs(h - abs(leg)) for h in legs) if legs else 1e9)
a, b = mt["marks"]
ia = sorted(round((bb(o).Min.X + bb(o).Max.X) / 2 if axis == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[a])
flag(f"{name} {a}/{b} alternate from the edge (F3)", ia == want[0::2], f"{a} {len(ia)}")
for m, md in G["mid"].items():
lift = md.get("lift", 0)
got = sorted((round(bb(o).Max.Z, 1), round((bb(o).Min.X + bb(o).Max.X) / 2 if md["axis"] == "Y" else (bb(o).Min.Y + bb(o).Max.Y) / 2, 1)) for o in cl[m])
want = sorted((float(z) + lift, p) for z, ps in md["levels"].items() for p in ps)
flag(f"{m} levels / positions ({len(want)})", len(got) == len(want) and all(abs(g[0] - w[0]) <= 1 and abs(g[1] - w[1]) <= 1 for g, w in zip(got, want)),
f"{got[:3]} vs {want[:3]}")
b10 = G["b10"]
zs = sorted(round(bb(o).Min.Z, 1) for o in cl["B10"])
check("B10 levels (worst)", 0, worst(zs, sorted(b10["levels"])))
check("B10 loop size X (worst)", 0, max(abs(bb(o).Max.X - bb(o).Min.X - b10["size"]) for o in cl["B10"]) if cl["B10"] else 1e9)
b9 = G["b9"]
got9 = sorted((round(mid_(o)[0], 1), round(mid_(o)[1], 1)) for o in cl["B9"])
want9 = sorted((x, y) for k, y in enumerate(b9["rows"]) for x in (b9["cols_even"] if k % 2 == 0 else b9["cols_odd"]))
flag(f"B9 grid {len(want9)} (rows {len(b9['rows'])}, 8 / 7 per row)", len(got9) == len(want9) and all(abs(g[0] - w[0]) <= 1 and abs(g[1] - w[1]) <= 1 for g, w in zip(got9, want9)),
f"{len(got9)}")
check("B9 height top-bottom (worst)", b9["height"], max((bb(o).Max.Z - bb(o).Min.Z for o in cl["B9"]), key=lambda h: abs(h - b9["height"])) if cl["B9"] else 0)
check("B9 span along Y a (worst)", b9["a"], max((bb(o).Max.Y - bb(o).Min.Y for o in cl["B9"]), key=lambda h: abs(h - b9["a"])) if cl["B9"] else 0)
flag("B9 hooks the top and bottom Y bars", all(bb(o).Max.Z > G["mats"]["y_top"]["z"] and bb(o).Min.Z < G["mats"]["y_bottom"]["z"] for o in cl["B9"]))
# containment and cover
ft = [o.Geometry for o in all_objects(doc) if (o.Attributes.GetUserString("id") or "") == f"{PID}-FT"][0]
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() / 300)), 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 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 the footing", 0, bad, tol=0)
info("min cover to the footing surface per mark (record)", json.dumps(cover))
write_result(RESULT_PATH, results, dict(cover=cover))
main()
@@ -0,0 +1,138 @@
"""G4 gate checker: coping rebar C1-C16, S1, S2 (check C10) inside Rhino 8.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values from EXPECTED_PATH section G4 (D3 counts, L,
a/b ranges; D1/D2 dimension chains). Width decisions (K1) make the original-length checks of the range marks
fail by design; they are reported as FAIL and listed in `planned_fail`. Generator params are not read.
Ported from Code/교각모델링/verify_coping_rebar.py (same checks and order).
"""
import json
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, load_expected, series, write_result, ychain # noqa: E402
doc = __rhino_doc__
X = load_expected(EXPECTED_PATH)
PID, G = X["pier_id"], X["G4"]
COUNT, L, DIA = G["count"], G["L"], G["dia"]
RANGE = {m: tuple(v) for m, v in G["range"].items()}
Y24 = ychain(G["y24_chain"], G["half_width"])
Y13 = ychain(G["y13_chain"], G["half_width"])
TIER = G["tier_z"]
STIR_X = {m: sorted(series(v)) for m, v in G["stirrup_x"].items()}
LOOP_Z = {m: series(v) for m, v in G["loop_z"].items()}
PALETTE = {int(k): tuple(v) for k, v in X["rebar_palette"].items()}
results = []
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 main():
objs = all_objects(doc)
cl = {m: [o for o in objs if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-RC-")
and o.Attributes.GetUserString("mark") == m] for m in COUNT}
pp = {m: [o for o in objs if (o.Attributes.GetUserString("id") or "").startswith(f"{PID}-R-")
and o.Attributes.GetUserString("mark") == m] for m in COUNT}
for m in COUNT:
check(f"{m} bar count", COUNT[m], 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]))
if not cl[m]:
continue
lens = [float(o.Attributes.GetUserString("design_length")) for o in cl[m]]
if m in RANGE:
key, hi, lo = RANGE[m]
vals = [float(o.Attributes.GetUserString(key)) for o in cl[m]]
check(f"{m} {key} max vs schedule", hi, max(vals))
check(f"{m} {key} min vs schedule", lo, min(vals))
cv = G["range_convention"][key]
fixed = cv["a"] if key == "b_sched" else cv["b"]
avg_conv = 2 * fixed + (hi + lo) + cv["c_times"] * cv["c"] # schedule convention: (max+min)/2
check(f"{m} schedule L reproduced by convention (max+min)/2", L[m], avg_conv)
ws = sorted({o.Attributes.GetUserString("w_model") for o in cl[m]})
results.append(dict(check="C10", item=f"{m} original length vs L (drawn widths {ws}, K1 decision)",
detail=f"model mean {sum(lens) / len(lens):.1f} vs L {L[m]}", result="FAIL"))
else:
check(f"{m} original length vs L (worst)", 0, max(abs(v - L[m]) for v in lens))
dev = [o.Geometry.GetLength() - float(o.Attributes.GetUserString("design_length")) for o in cl[m]]
info(f"{m} curve - original length", f"{min(dev):.1f} / {max(dev):.1f}")
for m, z in TIER.items():
zs = [o.Geometry.PointAt(o.Geometry.Domain.Mid).Z for o in cl[m]]
check(f"{m} tier EL (worst)", 0, max(abs(v - z) for v in zs))
for pair in G["pairs"]:
ys = sorted((round(o.Geometry.PointAt(o.Geometry.Domain.Mid).Y), m) for m in pair for o in cl[m])
flag(f"{'/'.join(pair)} Y = A-A chain, alternate from {pair[0]} at {Y24[0]:.0f}",
[y for y, _ in ys] == Y24 and [m for _, m in ys] == [pair[i % 2] for i in range(len(Y24))])
for m, xs in STIR_X.items():
got = sorted({round(o.Geometry.GetBoundingBox(True).Center.X) for o in cl[m]})
flag(f"{m} stirrup X positions", got == xs, f"{len(got)} positions")
per = {}
for o in cl[m]:
k = round(o.Geometry.GetBoundingBox(True).Center.X)
per[k] = per.get(k, 0) + 1
flag(f"{m} {G['stirrups_per_x']} stirrups per position", set(per.values()) == {G["stirrups_per_x"]})
zb = [round(o.Geometry.GetBoundingBox(True).Min.Z) for o in cl["C10"]]
check(f"C10 stirrup bottom EL {G['c10_bottom'] / 1000:.3f} (worst)", 0, max(abs(v - G["c10_bottom"]) for v in zb))
for m, zs in LOOP_Z.items():
got = sorted({round(o.Geometry.GetBoundingBox(True).Center.Z) for o in cl[m]})
flag(f"{m} loop levels", got == zs, str(got))
for m in ("C15", "C16"):
got = sorted(round(o.Geometry.GetBoundingBox(True).Center.Y) for o in cl[m])
flag(f"{m} Y = B-B chain", got == Y13, f"{len(got)}")
check(f"C15 EL {G['c15_top'] / 1000:.3f}", G["c15_top"], max(o.Geometry.GetBoundingBox(True).Max.Z for o in cl["C15"]))
check(f"C16 EL {G['c16_top'] / 1000:.3f}", G["c16_top"], max(o.Geometry.GetBoundingBox(True).Max.Z for o in cl["C16"]))
# horizontal-segment height (bbox max picks up upward hooks)
c14z = sorted(round(o.Geometry.PointAt(o.Geometry.Domain.Mid).Z, 1) for o in cl["C14"])
exp14 = sorted(z for z, n in G["c14_levels"] for _ in range(n))
flag("C14 levels/count per level", c14z == exp14, "")
step = {int(k): v for k, v in G["step_top"].items()}
for m, (n_per, dz) in G["s_per_girder"].items():
by = {}
for o in cl[m]:
g = int(o.Attributes.GetUserString("occurrence")[1])
by.setdefault(g, []).append(o.Geometry.GetBoundingBox(True).Max.Z)
flag(f"{m} per girder line = {n_per}", sorted(len(v) for v in by.values()) == [n_per] * len(step))
check(f"{m} EL = step top - {dz} (worst)", 0, max(abs(z - (step[g] - dz)) for g, v in by.items() for z in v))
cp = [o for o in objs if o.Attributes.GetUserString("id") == f"{PID}-CP"][0].Geometry
cover = {}
for m in COUNT:
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 cp.IsPointInside(q, 0.01, False):
bad += 1
cov = min(cov, p.DistanceTo(cp.ClosestPoint(p)) - r)
cover[m] = round(cov, 1)
check(f"{m} sample points outside coping", 0, bad, tol=0)
info("min cover to coping surface per mark (top face under pedestals counted)", json.dumps(cover))
wrong = 0
for o in objs:
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[d]:
wrong += 1
check("rebar colour mismatches (all rebar)", 0, wrong, tol=0)
write_result(RESULT_PATH, results, dict(cover=cover))
main()
@@ -0,0 +1,222 @@
"""G4 gate checker, Okseong P1 coping + bearing-seat rebar (2026-10-14): C1~C20 and S1~S8 as centreline polylines (plan skew 18 deg).
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4, plus the G1 pedestal table) come from the schedule
(L, N, polyline length = L - lap), the drawn levels / positions of the rebar drawing, the section dimension chains and the user
decisions H1-H6; generator params are not read. Coping bars are sheared like the coping: x_model = x - tan(skew) * y, so the
section x of a bar is bbox mid X + tan * bbox mid Y. Seat bars are checked in the pedestal frame.
Checks (C10): counts / pipes / polyline length, top layers (rows, ends, legs), C19 / C20, C7, stirrups (x, top, bottom, y), side loops
and ties, C18, seat bars per pedestal, containment in the coping + pedestals, minimum 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 = X["pier_id"], X["G4"]
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
T = math.tan(math.radians(G["skew_deg"]))
COS = math.cos(math.radians(G["skew_deg"]))
results = []
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 bb(o):
return o.Geometry.GetBoundingBox(True)
def mid(o):
b = bb(o)
return (b.Min.X + b.Max.X) / 2, (b.Min.Y + b.Max.Y) / 2
def xs(o):
"""Section x of a coping bar (shear removed)."""
mx, my = mid(o)
return mx + T * my
def worst(got, want):
got, want = sorted(got), sorted(want)
return max((abs(a - b) for a, b in zip(got, want)), default=1e9) if len(got) == len(want) else 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 m in G["fixed_length"]:
check(f"{m} polyline length vs L - lap (worst)", 0, max(abs(v - G["fixed_length"][m]) for v in dl) if dl else 1e9)
elif dl:
info(f"{m} mean polyline 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 C1~C6
tp, y25 = G["top"], G["y25"]
for m in ("C1", "C2", "C3", "C4", "C5", "C6"):
s = tp[m]
want = [y for i, y in enumerate(y25) if i % 2 == s["parity"]]
got = [mid(o)[1] for o in cl[m]]
check(f"{m} y rows vs chain (worst, n={len(got)}/{len(want)})", 0, worst(got, want))
check(f"{m} level of the straight part (worst)", 0, max(abs(bb(o).Max.Z - s["z"]) for o in cl[m]))
check(f"{m} leg {s['leg']} (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - s["leg"]) for o in cl[m]))
check(f"{m} x ends +-{tp['x_end']} sheared (worst)", 0,
max(max(abs(bb(o).Min.X + T * mid(o)[1] + tp["x_end"]), abs(bb(o).Max.X + T * mid(o)[1] - tp["x_end"])) for o in cl[m]))
# C19, C20
c19 = G["c19"]
check("C19 y rows vs chain (worst)", 0, worst([mid(o)[1] for o in cl["C19"]], y25))
check("C19 level (worst)", 0, max(abs(bb(o).Max.Z - c19["z"]) for o in cl["C19"]))
check("C19 x ends (worst)", 0, max(max(abs(bb(o).Min.X + T * mid(o)[1] - c19["x"][0]), abs(bb(o).Max.X + T * mid(o)[1] - c19["x"][1])) for o in cl["C19"]))
check("C19 leg 370 (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - G["c20_leg"]) for o in cl["C19"]))
for k, pc in enumerate(G["c20"]):
grp = [o for o in cl["C20"] if abs(bb(o).Max.Z - pc["z"]) < 0.5 and abs(bb(o).Min.X + T * mid(o)[1] - pc["x"][0]) < 3]
flag(f"C20 piece {k + 1}: 25 bars at EL {pc['z']}, x {pc['x']}", len(grp) == 25, f"{len(grp)}")
if grp:
check(f"C20 piece {k + 1} x ends (worst)", 0, max(max(abs(bb(o).Min.X + T * mid(o)[1] - pc["x"][0]), abs(bb(o).Max.X + T * mid(o)[1] - pc["x"][1])) for o in grp))
check(f"C20 piece {k + 1} leg 370 (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - G["c20_leg"]) for o in grp))
check(f"C20 piece {k + 1} y rows (worst)", 0, worst([mid(o)[1] for o in grp], y25))
# C7
c7 = G["c7"]
check("C7 lowest level per bar vs drawn (worst)", 0, worst([bb(o).Min.Z for o in cl["C7"]], c7["flat_z"]))
normal = [o for o in cl["C7"] if abs(mid(o)[1]) < 1400]
check("C7 top end EL 20.3672 (bars not on the side face, worst)", 0,
max(abs(bb(o).Max.Z - (c7["top_end_z"] + (0 if abs(mid(o)[1]) <= 1000 else 365.2))) for o in normal))
check("C7 x ends +-5875 (worst)", 0, max(max(abs(bb(o).Min.X + T * mid(o)[1] + c7["x_end"]), abs(bb(o).Max.X + T * mid(o)[1] - c7["x_end"])) for o in normal))
edge = [o for o in cl["C7"] if abs(mid(o)[1]) >= 1400]
check("C7 side-face bars end where the slope meets the coping top (x end)", 0,
max(max(abs(bb(o).Min.X + T * mid(o)[1] + c7["capped"]["x_end"]), abs(bb(o).Max.X + T * mid(o)[1] - c7["capped"]["x_end"])) for o in edge) if edge else 1e9, tol=8)
got = sorted(round(mid(o)[1]) for o in cl["C7"])
flag("C7 y rows: 11 @250 + 2 on the side face", got == sorted([-1250 + 250 * k for k in range(11)] + [-1525, 1525]), f"{got}")
# stirrups
st = G["stirrups"]
def top_of(x):
return next(z for a, b, z in st["top"] if a <= x < b)
def bottom_of(x):
s = st["bottom"]
return s["flat_z"] if abs(x) <= s["flat_half"] else s["flat_z"] + s["slope"] * (abs(x) - s["flat_half"])
for zone, (rect, pent) in st["marks"].items():
want = st["x"][zone]
got_r = [xs(o) for o in cl[rect]]
check(f"{rect} positions = drawn x ({len(want)}, worst)", 0, worst(got_r, want))
got_p = [xs(o) - st["pent_dx"] for o in cl[pent]]
check(f"{pent} positions = drawn x + {st['pent_dx']}, 2 per x (worst)", 0, worst(got_p, [x for x in want for _ in (0, 1)]))
dt = max(abs(bb(o).Max.Z - top_of(xs(o))) for o in cl[rect] + cl[pent]) if cl[rect] else 1e9
db = max(abs(bb(o).Min.Z - bottom_of(xs(o) - (st["pent_dx"] if o in cl[pent] else 0))) for o in cl[rect] + cl[pent]) if cl[rect] else 1e9
check(f"{rect}/{pent} top = coping top - 97.5 (drawn zones, worst)", 0, dt)
check(f"{rect}/{pent} bottom = soffit + 120 - 22.5 (worst)", 0, db, tol=2.5)
check(f"{rect} legs y +-{st['rect_y']} (worst)", 0, max(max(abs(bb(o).Max.Y - st["rect_y"] - 0), abs(bb(o).Min.Y + st["rect_y"])) for o in cl[rect]) if cl[rect] else 1e9, tol=2)
lo, hi = st["pent_y"]
okp = all((abs(bb(o).Min.Y - lo) < 2 and abs(bb(o).Max.Y - hi) < 2) or (abs(bb(o).Max.Y + lo) < 2 and abs(bb(o).Min.Y + hi) < 2) for o in cl[pent])
flag(f"{pent} y between +-{lo} and +-{hi} (A-A)", okp)
# side loops and ties
lp = G["loops"]
for key in ("upper", "lower"):
sec = lp[key]
mloop, mtie = sec["marks"]
gl = sorted((round(bb(o).Min.Z, 1), round(bb(o).Max.X - bb(o).Min.X - 2 * T * (bb(o).Max.Y - bb(o).Min.Y) / 2, 1), round((bb(o).Max.Y - bb(o).Min.Y) / 2, 1)) for o in cl[mloop])
wl = sorted((z, round(2 * x, 1), y) for z, x, y in zip(sec["z"], sec["x"], sec["y"]))
flag(f"{mloop} loops: level / x length (corner fillets <= 50) / half width ({len(wl)})", len(gl) == len(wl) and all(abs(a[0] - b[0]) <= 1 and abs(a[1] - b[1]) <= 50 and abs(a[2] - b[2]) <= 1 for a, b in zip(gl, wl)), f"{gl[:2]}")
flat = max((bb(o).Max.Z - bb(o).Min.Z) for o in cl[mloop] + cl[mtie])
check(f"{mloop}/{mtie} horizontal (worst dz)", 0, flat, tol=0.01)
gt = sorted((round(bb(o).Min.Z, 1), round(mid(o)[1], 1), round(bb(o).Max.X - bb(o).Min.X, 1)) for o in cl[mtie])
wt = sorted((z, y, round(2 * x, 1)) for z, x in zip(sec["z"], sec["x"]) for y in lp["tie_y"])
flag(f"{mtie} ties: level / y / x length ({len(wt)})", len(gt) == len(wt) and all(abs(a[0] - b[0]) <= 1 and abs(a[1] - b[1]) <= 1 and abs(a[2] - b[2]) <= 1 for a, b in zip(gt, wt)), f"{len(gt)}")
# C18
c18 = G["c18"]
got = {}
for o in cl["C18"]: # lower rows have the legs UP (the straight part is the lowest level)
z = bb(o).Max.Z if bb(o).Max.Z - bb(o).Min.Z < 1 or abs((bb(o).Max.Z - bb(o).Min.Z) - 370) < 1 and bb(o).Min.Z > 20000 else bb(o).Min.Z
got.setdefault(f"{z:.1f}", []).append(o)
flag("C18 levels and counts", {k: len(v) for k, v in got.items()} == c18["levels"], f"{ {k: len(v) for k, v in got.items()} }")
check("C18 widths per level (worst; fillets on the slanted legs <= 30)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - c18["width"][k]) for k, v in got.items() if k in c18["width"] for o in v), tol=30)
# seats
se = G["seats"]
ped = {}
for o in all_objects(doc):
oid = o.Attributes.GetUserString("id") or ""
if oid.startswith(f"{PID}-BS"):
ped[int(oid[len(PID) + 3:])] = o.Geometry.GetBoundingBox(True)
flag("5 pedestals found", sorted(ped) == [1, 2, 3, 4, 5], f"{sorted(ped)}")
for k, pb in sorted(ped.items()):
cx, cy = (pb.Min.X + pb.Max.X) / 2, (pb.Min.Y + pb.Max.Y) / 2
yf, yr = se["y"]["front"], se["y"]["rear"]
zp = pb.Min.Z + se["h1"]["front"] + (se["h1"]["rear"] - se["h1"]["front"]) * (cy - yf) / (yr - yf)
ml, ms, mll, mss = se["marks_heavy"] if k == se["heavy"] else se["marks_light"]
dl_, ds_ = G["dia"][ml], G["dia"][ms]
grp = lambda m: [o for o in cl[m] if o.Attributes.GetUserString("occurrence").startswith(f"P{k}-")]
gl, gs, gll, gss = grp(ml), grp(ms), grp(mll), grp(mss)
flag(f"pedestal {k}: {ml} {ms} {mll} {mss} counts 11 / 21 / 22 / 42", (len(gl), len(gs), len(gll), len(gss)) == (11, 21, 22, 42), f"{len(gl)} {len(gs)} {len(gll)} {len(gss)}")
check(f"pedestal {k} {ml} level {se['top_below']} below the pedestal top (worst)", 0, max(abs(bb(o).Max.Z - (zp - se["top_below"])) for o in gl), tol=1.5)
check(f"pedestal {k} {ml} legs {se['leg']} (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - se["leg"]) for o in gl + gs))
check(f"pedestal {k} {ml} lateral positions (worst)", 0, worst([(mid(o)[0] - cx) * COS for o in gl], se["long_t"]), tol=1.5)
check(f"pedestal {k} {ml} length along the end face (worst)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - se["long_len"] * COS) for o in gl), tol=1.5)
check(f"pedestal {k} {ms} level {ds_ if False else dl_} under {ml} (worst)", 0, max(abs(bb(o).Max.Z - (zp - se["top_below"] - dl_)) for o in gs), tol=1.5)
check(f"pedestal {k} {ms} y positions (worst)", 0, worst([mid(o)[1] - cy for o in gs], se["short_o"]), tol=1.5)
check(f"pedestal {k} {ms} x length (worst)", 0, max(abs((bb(o).Max.X - bb(o).Min.X) - se["short_len"]) for o in gs), tol=1.5)
for layer, dz in enumerate(se["lower_below"]):
l1 = [o for o in gll if abs(bb(o).Min.Z - (zp - dz)) < 1.5]
l2 = [o for o in gss if abs(bb(o).Min.Z - (zp - dz + (G["dia"][mll] + G["dia"][mss]) / 2)) < 1.5]
flag(f"pedestal {k} layer {layer + 1} EL pedestal top - {dz}: {mll} 11 / {mss} 21", (len(l1), len(l2)) == (11, 21), f"{len(l1)} {len(l2)}")
if l1:
check(f"pedestal {k} {mll} layer {layer + 1} positions (worst)", 0, worst([(mid(o)[0] - cx) * COS for o in l1], se["low_long_t"]), tol=1.5)
check(f"pedestal {k} {mll} layer {layer + 1} length (worst)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - se["low_long_len"] * COS) for o in l1), tol=1.5)
if l2:
check(f"pedestal {k} {mss} layer {layer + 1} y positions (worst)", 0, worst([mid(o)[1] - cy for o in l2], se["low_short_o"]), tol=1.5)
# 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]
info("concrete solids for containment", f"{len(solids)} (coping + pedestals)")
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() / 200)), 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):
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()
@@ -0,0 +1,207 @@
"""G4 gate checker, form "polylines" (Ulsan P1, 2026-10-13): coping rebar C1~C15 and bearing-seat S1~S4.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4) are the schedule (L, n, ranges)
and drawn coordinates of the rebar drawing (front / side views, sections, seat details); params are not read.
Checks (C10): counts, fixed-length marks vs L, variable dimension ranges vs schedule (record mean length),
top / bottom layer level, y rows and x extents, stirrup positions / pairs / heights, side-bar and hoop levels
and ends, C14 positions, seat bars, containment in the coping + pedestals, minimum cover (record).
"""
import json
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 = X["pier_id"], X["G4"]
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
results = []
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(spec):
return [spec["start"] + spec["pitch"] * i for i in range(spec["n"])]
def bb(o):
return o.Geometry.GetBoundingBox(True)
def worst(pairs):
return max((abs(a - b) for a, b in pairs), default=1e9)
def match(got, exp, tol=1.0):
"""Every expected tuple has one got tuple within tol (and same count)."""
got = list(got)
if len(got) != len(exp):
return False
for e in exp:
k = next((i for i, g in enumerate(got) if all(abs(a - b) <= tol for a, b in zip(g, e))), None)
if k is None:
return False
got.pop(k)
return True
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)
dl = {m: [float(o.Attributes.GetUserString("design_length")) for o in cl[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 not dl[m]:
continue
if m in G["fixed_length"]:
check(f"{m} original length vs L (worst)", 0, max(abs(v - L) for v in dl[m]))
else:
info(f"{m} mean original length vs L (variable mark, record)",
f"mean {statistics.mean(dl[m]):.1f} / L {L} (diff {statistics.mean(dl[m]) - L:+.1f}); min {min(dl[m]):.0f} max {max(dl[m]):.0f}")
# top / bottom layers
rows = {k: row(v) for k, v in G["rows"].items()}
cy_abs = G["corner"]["y_abs"]
clipped_x = set(G["corner"]["x_clipped"])
for m, lay in G["layers"].items():
if not cl[m]:
continue
ys = rows[lay["y"]]
if lay["parity"] is not None:
ys = [y for i, y in enumerate(ys) if i % 2 == lay["parity"]]
got = sorted(round((bb(o).Min.Y + bb(o).Max.Y) / 2, 1) for o in cl[m])
check(f"{m} y rows vs chain (worst, n={len(got)}/{len(ys)})", 0, worst(zip(got, ys)) if len(got) == len(ys) else 1e9)
if "z" in lay:
check(f"{m} level (top straight, worst)", 0, max(abs(bb(o).Max.Z - lay["z"]) for o in cl[m]))
else:
check(f"{m} bottom level (worst)", 0, max(abs(bb(o).Min.Z - lay["z_min"]) for o in cl[m]))
if m in G["x_extent"]:
x0, x1 = G["x_extent"][m]
inner = [o for o in cl[m] if abs((bb(o).Min.Y + bb(o).Max.Y) / 2) <= cy_abs]
check(f"{m} drawn x ends, |y| <= {cy_abs} (worst; corner fillets)", 0,
max(max(abs(bb(o).Min.X - x0), abs(bb(o).Max.X - x1)) for o in inner), tol=12)
info(f"{m} bars shortened at the R500 plan corners [추론]",
f"{sum(1 for o in cl[m] if o not in inner and bb(o).Max.X - bb(o).Min.X < x1 - x0 - 30)}"
f" of {len(cl[m]) - len(inner)} outer bars")
# stirrups
st = G["stirrups"]
for m, xs in st["x"].items():
got = {}
for o in cl[m]:
b = bb(o)
got.setdefault(round((b.Min.X + b.Max.X) / 2), []).append(o)
flag(f"{m} stirrup positions = drawn x ({len(xs)}) x 3", sorted(got) == sorted(round(x) for x in xs)
and all(len(v) == 3 for v in got.values()), f"{len(got)} positions")
pairs_ok, hw = True, 0
for x, os_ in got.items():
if x in clipped_x:
continue
pairs_ok &= match([(bb(o).Min.Y, bb(o).Max.Y) for o in os_], [tuple(p) for p in st["y_pairs"]])
exp_h = st["height"].get(f"{x:.0f}")
hw = max([hw] + [abs((bb(o).Max.Z - bb(o).Min.Z) - exp_h) for o in os_]) if exp_h else 1e9
flag(f"{m} legs at y pairs {st['y_pairs']} (A-A; corner positions {sorted(clipped_x)} excluded)", pairs_ok)
check(f"{m} centreline height vs drawn + 54 (worst)", 0, hw)
# variable ranges (schedule)
for m, (dim, lo, hi) in G["ranges"].items():
if not cl[m]:
continue
if dim == "B":
vals = [bb(o).Max.Z - bb(o).Min.Z for o in cl[m]]
check(f"{m} B min vs schedule '{lo}~{hi}'", lo, min(vals), tol=2)
check(f"{m} B max vs schedule '{lo}~{hi}'", hi, max(vals), tol=2)
else:
vals = []
for o in cl[m]:
b = bb(o)
a = b.Max.X - b.Min.X + (44 if m in ("C11", "C13") else (54 if m == "C10" else 44 - 790))
vals.append(a)
rng = f"model {min(vals):.0f}~{max(vals):.0f} / schedule {lo}~{hi}"
ok = abs(min(vals) - lo) <= 2 and abs(max(vals) - hi) <= 2
results.append(dict(check="C10", item=f"{m} A range vs schedule", detail=rng,
result="PASS" if ok else "INFO"))
# side bars and hoops per level
lv = G["levels"]
for key, yset in (("side", G["side"]["y"]), ("hoops", None)):
k0 = 0
for m, nlev in G[key]["marks"]:
exp = lv[k0:k0 + nlev]
k0 += nlev
got = []
for o in cl[m]:
b = bb(o)
c = o.Geometry.PointAt(o.Geometry.Domain.Mid)
got.append((round(b.Min.Z, 1), b.Min.X, b.Max.X, c.Y if yset else b.Max.Y - b.Min.Y))
want = [(zz, xl, xr, y) for zz, xl, xr in exp for y in (yset or [2 * G["hoops"]["y"]])]
flag(f"{m} levels / drawn ends / y ({len(want)})", match(got, want), f"{len(got)} bars")
# C14
c = G["c14"]
got = [((bb(o).Min.X + bb(o).Max.X) / 2, bb(o).Min.Z, bb(o).Min.Y, bb(o).Max.Y) for o in cl["C14"]]
n_corner = sum(1 for p in c["pts"] if round(abs(p[0]) + 19) in clipped_x)
full = [g for g in got if abs(g[2] - c["y"][0]) <= 1 and abs(g[3] - c["y"][1]) <= 1]
flag("C14 levels (drawn section marks)", match([(g[1],) for g in got], [(p[1],) for p in c["pts"]]), f"{len(got)}")
flag(f"C14 y span {c['y']} except {n_corner} corner bars", len(full) == len(got) - n_corner
and all(g[2] >= c["y"][0] - 1 and g[3] <= c["y"][1] + 1 for g in got), f"{len(full)} full")
# seat bars
se = G["seats"]
for no, s in se["bearings"].items():
cx, cy = s["centre"]
my, mx = s["marks"]
oy = [o for o in cl[my] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}")]
ox = [o for o in cl[mx] if o.Attributes.GetUserString("occurrence").startswith(f"B{no}")]
flag(f"bearing {no} {my} x positions / y span", match([((bb(o).Min.X + bb(o).Max.X) / 2, bb(o).Min.Y, bb(o).Max.Y) for o in oy],
[(cx + d, cy - se["half"], cy + se["half"]) for d in se["offsets"]]), f"{len(oy)}")
flag(f"bearing {no} {mx} y positions / x span", match([((bb(o).Min.Y + bb(o).Max.Y) / 2, bb(o).Min.X, bb(o).Max.X) for o in ox],
[(cy + d, cx - se["half"], cx + se["half"]) for d in se["offsets"]]), f"{len(ox)}")
check(f"bearing {no} {my} level 50 below pedestal top (worst)", 0, max(abs(bb(o).Max.Z - s["z_top"]) for o in oy))
check(f"bearing {no} legs {se['leg']} (worst)", se["leg"], min(bb(o).Max.Z - bb(o).Min.Z for o in oy + ox))
# 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]
info("concrete solids for containment", f"{len(solids)} (coping + pedestals)")
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):
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 coping surface per mark (record; seat bars measured to the coping only)", json.dumps(cover))
write_result(RESULT_PATH, results, dict(cover=cover))
main()
@@ -0,0 +1,185 @@
"""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()
@@ -0,0 +1,207 @@
"""G4 gate checker, Saejae-1 P1 coping + bearing-seat rebar (2026-10-15): C1~C20 and S1~S2 as centreline polylines (rectangular plan, no skew).
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values (section G4) come from the schedule (L, N, polyline length = a + legs),
the drawn levels / positions of the rebar drawing, the section dimension chains and the Okseong rules H1-H6 applied again; generator params
are not read. Checks (C10): counts / pipes / polyline length, top layers, C19 / C20, C7, stirrups (x, top, bottom, y), side loops and ties,
C18, seat bars per pedestal, containment in the coping + pedestals, minimum cover (record).
"""
import json
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 = X["pier_id"], X["G4"]
SCHED = {m: tuple(v) for m, v in G["schedule"].items()}
results = []
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 bb(o):
return o.Geometry.GetBoundingBox(True)
def mid(o):
b = bb(o)
return (b.Min.X + b.Max.X) / 2, (b.Min.Y + b.Max.Y) / 2
def worst(got, want):
got, want = sorted(got), sorted(want)
return max((abs(a - b) for a, b in zip(got, want)), default=1e9) if len(got) == len(want) else 1e9
def occ(o):
return o.Attributes.GetUserString("occurrence") or ""
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 m in G["fixed_length"]:
check(f"{m} polyline length vs a + legs (worst)", 0, max(abs(v - G["fixed_length"][m]) for v in dl) if dl else 1e9)
elif dl:
info(f"{m} mean polyline 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 C1~C6
tp, y23 = G["top"], G["y23"]
for m in ("C1", "C2", "C3", "C4", "C5", "C6"):
s = tp[m]
want = [y for i, y in enumerate(y23) if i % 2 == s["parity"]]
check(f"{m} y rows vs chain (worst, n={len(cl[m])}/{len(want)})", 0, worst([mid(o)[1] for o in cl[m]], want))
check(f"{m} level of the straight part (worst)", 0, max(abs(bb(o).Max.Z - s["z"]) for o in cl[m]))
check(f"{m} leg {s['leg']} (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - s["leg"]) for o in cl[m]))
check(f"{m} x ends +-{tp['x_end']} (worst)", 0, max(max(abs(bb(o).Min.X + tp["x_end"]), abs(bb(o).Max.X - tp["x_end"])) for o in cl[m]))
# C19, C20
c19 = G["c19"]
check("C19 y rows vs chain (worst)", 0, worst([mid(o)[1] for o in cl["C19"]], y23))
check("C19 level (worst)", 0, max(abs(bb(o).Max.Z - c19["z"]) for o in cl["C19"]))
check("C19 x ends (worst)", 0, max(max(abs(bb(o).Min.X - c19["x"][0]), abs(bb(o).Max.X - c19["x"][1])) for o in cl["C19"]))
check("C19 legs 370 (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - G["c20_leg"]) for o in cl["C19"]))
for k, pc in enumerate(G["c20"]):
grp = [o for o in cl["C20"] if abs(bb(o).Max.Z - pc["z"]) < 0.5 and abs(bb(o).Min.X - pc["x"][0]) < 3]
flag(f"C20 piece {k + 1}: 23 bars at EL {pc['z']}, x {pc['x']}", len(grp) == 23, f"{len(grp)}")
if grp:
check(f"C20 piece {k + 1} x ends (worst)", 0, max(max(abs(bb(o).Min.X - pc["x"][0]), abs(bb(o).Max.X - pc["x"][1])) for o in grp))
check(f"C20 piece {k + 1} leg 370 (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - G["c20_leg"]) for o in grp))
check(f"C20 piece {k + 1} y rows (worst)", 0, worst([mid(o)[1] for o in grp], y23))
# C7
c7 = G["c7"]
check("C7 lowest level per bar vs drawn (worst)", 0, worst([bb(o).Min.Z for o in cl["C7"]], c7["flat_z"]))
flat = [o for o in cl["C7"] if abs(mid(o)[1]) <= 875]
check("C7 top end EL 31.8453 (flat bars, worst)", 0, max(abs(bb(o).Max.Z - c7["top_end_z"]) for o in flat))
check("C7 x ends +-5875 (flat bars, worst)", 0, max(max(abs(bb(o).Min.X + c7["x_end"]), abs(bb(o).Max.X - c7["x_end"])) for o in flat))
raised = [o for o in cl["C7"] if 875 < abs(mid(o)[1]) <= 1125]
check("C7 raised bars (y +-1125) top end (worst)", 0, max(abs(bb(o).Max.Z - c7["raised_top_z"]) for o in raised))
edge = [o for o in cl["C7"] if abs(mid(o)[1]) > 1125]
check("C7 side-face bars capped at the cap level (worst)", 0, max(abs(bb(o).Max.Z - c7["cap_z"]) for o in edge), tol=1.5)
flag("C7 y rows: 12 @250", sorted(round(mid(o)[1]) for o in cl["C7"]) == [-1375 + 250 * k for k in range(12)])
# stirrups
st = G["stirrups"]
def zone(x):
return "left" if x < st["zone_cut"][0] else ("centre" if x <= st["zone_cut"][1] else "right")
def top_of(x):
return st["step_z"][sum(1 for b in st["step_b"] if x >= b)] - st["top_below"]
def bottom_of(x):
if abs(x) <= st["flat_half"]:
return st["flat_bottom"]
return st["flat_bottom"] - st["cover_n"] + st["soffit_slope"] * (abs(x) - st["flat_half"]) + st["cover_n"] * (1 + st["soffit_slope"] ** 2) ** 0.5
for zn, (rect, pent) in st["marks"].items():
sx = [x for x in st["x"] if zone(x) == zn]
want_r = [x + o for x in sx for o in st["off"]["R"]]
want_p = [x + o for x in sx for o in st["off"]["P"]]
check(f"{rect} positions = stations + x offset ({len(want_r)}, worst)", 0, worst([mid(o)[0] for o in cl[rect]], want_r), tol=0.5)
check(f"{pent} positions = stations + x offset ({len(want_p)}, worst)", 0, worst([mid(o)[0] for o in cl[pent]], want_p), tol=0.5)
dt = db = 0.0
for o in cl[rect] + cl[pent]:
xb = mid(o)[0]
dt = max(dt, abs(bb(o).Max.Z - top_of(xb)))
db = max(db, abs(bb(o).Min.Z - bottom_of(xb)))
check(f"{rect}/{pent} top = step top - {st['top_below']:.0f} (worst)", 0, dt, tol=1.5)
check(f"{rect}/{pent} bottom = soffit + cover along the normal (worst, sloped part <= 4)", 0, db, tol=4.0)
okr = all(any(abs(bb(o).Min.Y - a) < 2 and abs(bb(o).Max.Y - b) < 2 for a, b in st["rect_y"]) for o in cl[rect])
flag(f"{rect} y legs {st['rect_y']} (B-B)", okr)
pin, pout = st["pent_in"], st["pent_out"]
okp = all((abs(bb(o).Min.Y + pin) < 2 and abs(bb(o).Max.Y - pout) < 2) or (abs(bb(o).Max.Y - pin) < 2 and abs(bb(o).Min.Y + pout) < 2) for o in cl[pent])
flag(f"{pent} y between {pin:.0f} and {pout:.0f} (B-B)", okp)
# side loops and ties
lp = G["loops"]
for key in ("upper", "lower"):
sec = lp[key]
mloop, mtie = sec["marks"]
gl = sorted((round(bb(o).Min.Z, 1), round(bb(o).Max.X - bb(o).Min.X, 1), round((bb(o).Max.Y - bb(o).Min.Y) / 2, 1)) for o in cl[mloop])
wl = sorted((z, round(2 * x, 1), y) for z, x, y in zip(sec["z"], sec["x"], sec["y"]))
flag(f"{mloop} loops: level / x length / half width ({len(wl)})", len(gl) == len(wl) and all(abs(a[0] - b[0]) <= 1 and abs(a[1] - b[1]) <= 1 and abs(a[2] - b[2]) <= 1 for a, b in zip(gl, wl)), f"{gl[:2]}")
check(f"{mloop}/{mtie} horizontal (worst dz)", 0, max((bb(o).Max.Z - bb(o).Min.Z) for o in cl[mloop] + cl[mtie]), tol=0.01)
gt = sorted((round(bb(o).Min.Z, 1), round(mid(o)[1], 1), round(bb(o).Max.X - bb(o).Min.X, 1)) for o in cl[mtie])
wt = sorted((z, y, round(2 * x, 1)) for z, x in zip(sec["z"], sec["x"]) for y in lp["tie_y"])
flag(f"{mtie} ties: level / y / x length ({len(wt)})", len(gt) == len(wt) and all(abs(a[0] - b[0]) <= 1 and abs(a[1] - b[1]) <= 1 and abs(a[2] - b[2]) <= 1 for a, b in zip(gt, wt)), f"{len(gt)}")
# C18
c18 = G["c18"]
got = {}
for o in cl["C18"]: # upper rows: legs DOWN (straight part highest); lower rows: legs UP (lowest)
z = bb(o).Max.Z if bb(o).Max.Z > 32100 else bb(o).Min.Z
got.setdefault(f"{z:.1f}", []).append(o)
flag("C18 levels and counts", {k: len(v) for k, v in got.items()} == c18["levels"], f"{ {k: len(v) for k, v in got.items()} }")
check("C18 widths per level (worst; fillets on the slanted legs <= 30)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - c18["width"][k]) for k, v in got.items() if k in c18["width"] for o in v), tol=30)
# seats
se = G["seats"]
ped = {}
for o in all_objects(doc):
oid = o.Attributes.GetUserString("id") or ""
if oid.startswith(f"{PID}-BS"):
ped[int(oid[len(PID) + 3:])] = o.Geometry.GetBoundingBox(True)
flag("5 pedestals found", sorted(ped) == [1, 2, 3, 4, 5], f"{sorted(ped)}")
for k, pb in sorted(ped.items()):
cx, cy = (pb.Min.X + pb.Max.X) / 2, (pb.Min.Y + pb.Max.Y) / 2
g1 = [o for o in cl["S1"] if occ(o).startswith(f"P{k}-")]
g2 = [o for o in cl["S2"] if occ(o).startswith(f"P{k}-")]
flag(f"pedestal {k}: S1 11 / S2 20", (len(g1), len(g2)) == (11, 20), f"{len(g1)} {len(g2)}")
zt = pb.Max.Z - se["top_below"]
check(f"pedestal {k} S1 level {se['top_below']} below the pedestal top (worst, plane slope <= 20)", 0, max(abs(bb(o).Max.Z - zt) for o in g1), tol=20)
check(f"pedestal {k} S1 legs {se['leg']} (worst)", 0, max(abs((bb(o).Max.Z - bb(o).Min.Z) - se["leg"]) for o in g1 + g2))
check(f"pedestal {k} S2 level 13 under S1 (worst)", 0, max(abs(bb(o).Max.Z - (min(bb(p).Max.Z for p in g1) - 13)) for o in g2), tol=1.5)
check(f"pedestal {k} S1 x positions (worst)", 0, worst([mid(o)[0] - cx for o in g1], se["s1_t"]), tol=1.5)
check(f"pedestal {k} S1 length along Y (worst)", 0, max(abs((bb(o).Max.Y - bb(o).Min.Y) - se["s1_len"]) for o in g1), tol=1.5)
check(f"pedestal {k} S2 y positions (worst)", 0, worst([mid(o)[1] - cy for o in g2], se["s2_o"]), tol=1.5)
check(f"pedestal {k} S2 length along X (worst)", 0, max(abs((bb(o).Max.X - bb(o).Min.X) - se["s2_len"]) for o in g2), tol=1.5)
# 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]
info("concrete solids for containment", f"{len(solids)} (coping + pedestals)")
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() / 200)), 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):
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()
@@ -0,0 +1,135 @@
"""G5 gate checker: bridge bearings (BB) and sole plates (SP), check C11, inside Rhino 8.
Globals: __rhino_doc__, EXPECTED_PATH, RESULT_PATH. Expected values from EXPECTED_PATH section G5 (D2 ELEVATION
TABLE, 솔플레이트 상세 TABLE, user rules); orientation is compared with the modelled h2 pads (BP).
Generator params are not read. Ported from Code/교각모델링/verify_bearing_plates.py (same checks and order).
"""
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, load_expected # noqa: E402
doc = __rhino_doc__
X = load_expected(EXPECTED_PATH)
PID, G = X["pier_id"], X["G5"]
ELX = {int(k): v for k, v in G["el_x"].items()}
S = {int(k): v for k, v in G["S"].items()}
W = {int(k): v for k, v in G["W"].items()}
L, T, H3, SHRINK = G["L"], G["T"], G["h3"], G.get("bearing_shrink")
# per-bearing values (Ulsan 2026-10-13): L / T / h3 may be {no: value}; bearing_size {no: [along, across]} replaces
# the (L-shrink) x (W-shrink) rule; thick_end "-Y" (Ungok, default) or "+Y"
per = lambda v, no: v[str(no)] if isinstance(v, dict) else v
BSIZE = {int(k): v for k, v in G.get("bearing_size", {}).items()}
THICK = G.get("thick_end", "-Y")
COL_B, COL_S = tuple(G["colours"]["bearing"]), tuple(G["colours"]["sole_plate"])
results = []
def check(item, exp, act, tol=1.0):
results.append(dict(check="C11", item=item, expected=round(exp, 4), actual=round(act, 4),
diff=round(act - exp, 4), tol=tol, result="PASS" if abs(act - exp) <= tol else "FAIL"))
def flag(item, ok, detail=""):
results.append(dict(check="C11", item=item, detail=detail, result="PASS" if ok else "FAIL"))
def edges_xy(brep, top=False):
"""Return horizontal edge vectors of the lowest (or highest) face (4 edges). Pads use the top face: their bottom
may follow a sloped pedestal top (Okseong P1, 2026-10-14)."""
zref = (max if top else min)(v.Location.Z for v in brep.Vertices)
out = []
for e in brep.Edges:
a, b = e.PointAtStart, e.PointAtEnd
if abs(a.Z - zref) < 0.01 and abs(b.Z - zref) < 0.01 and a.DistanceTo(b) > 1:
out.append(b - a)
return out
def split_axes(vecs):
"""Bridge-axis (along) edge = within 45 deg of +Y; the other is across. Not by length (W > L occurs)."""
along = [v for v in vecs if abs(v.Y) > abs(v.X)]
across = [v for v in vecs if abs(v.Y) <= abs(v.X)]
return along[0], across[0]
def axis_deg(vec):
"""Angle of a plan vector from +Y, clockwise positive, folded to [-90, 90)."""
a = math.degrees(math.atan2(vec.X, vec.Y))
while a >= 90:
a -= 180
while a < -90:
a += 180
return a
def main():
objs = all_objects(doc)
by = lambda p: {int(o.Attributes.GetUserString("id")[len(p):]): o for o in objs
if (o.Attributes.GetUserString("id") or "").startswith(p)}
bb, sp, pad = by(f"{PID}-BB"), by(f"{PID}-SP"), by(f"{PID}-BP")
n = len(ELX)
check("bearing count", n, len(bb), tol=0)
check("sole plate count", n, len(sp), tol=0)
flag(f"all {2 * n} closed solids", all(o.Geometry.IsSolid for o in list(bb.values()) + list(sp.values())))
for no in sorted(ELX):
gb, gs, gp = bb[no].Geometry, sp[no].Geometry, pad[no].Geometry
bx, sx = gb.GetBoundingBox(True), gs.GetBoundingBox(True)
check(f"NO.{no} bearing bottom = EL.'X'", ELX[no], bx.Min.Z)
check(f"NO.{no} bearing height h3", per(H3, no), bx.Max.Z - bx.Min.Z, tol=0.01)
ab, cb_ = split_axes(edges_xy(gb))
if no in BSIZE:
check(f"NO.{no} bearing across (B)", BSIZE[no][1], cb_.Length, tol=0.5)
check(f"NO.{no} bearing along (A)", BSIZE[no][0], ab.Length, tol=0.5)
else:
check(f"NO.{no} bearing across (W-{SHRINK})", W[no] - SHRINK, cb_.Length, tol=0.5)
check(f"NO.{no} bearing along (L-{SHRINK})", per(L, no) - SHRINK, ab.Length, tol=0.5)
as_, cs_ = split_axes(edges_xy(gs))
check(f"NO.{no} sole plate across W", W[no], cs_.Length, tol=0.5)
check(f"NO.{no} sole plate along L", per(L, no), as_.Length, tol=0.5)
check(f"NO.{no} sole plate bottom = bearing top", bx.Max.Z, sx.Min.Z, tol=0.01)
long_b, long_s, long_p = ab, as_, split_axes(edges_xy(gp, top=True))[0]
pc = gp.GetBoundingBox(True).Center
for nm, g in (("bearing", gb), ("sole plate", gs)):
c = g.GetBoundingBox(True).Center
check(f"NO.{no} {nm} plan centre offset from pad (mm)", 0, math.hypot(c.X - pc.X, c.Y - pc.Y), tol=1.0)
check(f"NO.{no} bearing axis vs pad (deg)", axis_deg(long_p), axis_deg(long_b), tol=0.01)
check(f"NO.{no} sole plate axis vs pad (deg)", axis_deg(long_p), axis_deg(long_s), tol=0.01)
top = [v.Location for v in gs.Vertices if v.Location.Z > sx.Min.Z + 1]
ua = rg.Vector3d(long_s.X, long_s.Y, 0)
ua.Unitize()
if ua.Y < 0:
ua = -ua # along points to +Y
c = rg.Point3d(sum(p.X for p in top) / 4, sum(p.Y for p in top) / 4, 0)
proj = [((p - c) * ua, p.Z) for p in top]
lo = [z for s_, z in proj if s_ < 0]
hi = [z for s_, z in proj if s_ > 0]
slope = (sum(hi) / len(hi) - sum(lo) / len(lo)) / per(L, no) * 100
check(f"NO.{no} sole plate slope S (%) along +Y-ward axis", S[no], slope, tol=0.01)
check(f"NO.{no} sole plate centre thickness T", per(T, no), sum(z for _, z in proj) / 4 - sx.Min.Z, tol=0.01)
flag(f"NO.{no} thick end at {THICK}", sum(lo) > sum(hi) if THICK == "-Y" else sum(hi) > sum(lo))
pairs = sorted(proj)
check(f"NO.{no} no transverse slope", 0, max(abs(pairs[0][1] - pairs[1][1]), abs(pairs[2][1] - pairs[3][1])),
tol=0.01)
for a, b2, name in ((gb, gp, "bearing-pad"), (gb, gs, "bearing-sole plate")):
r = rg.Brep.CreateBooleanIntersection(a, b2, 0.01)
v = sum(rg.VolumeMassProperties.Compute(x).Volume for x in r) if r else 0.0
check(f"NO.{no} overlap {name} (mm3)", 0, v, tol=1.0)
cb, cs = bb[no].Attributes, sp[no].Attributes
rgb = lambda a: (a.ObjectColor.R, a.ObjectColor.G, a.ObjectColor.B)
flag(f"NO.{no} colours (bearing {COL_B}, sole plate {COL_S})",
cb.ColorSource == ObjectColorSource.ColorFromObject and rgb(cb) == COL_B
and cs.ColorSource == ObjectColorSource.ColorFromObject and rgb(cs) == COL_S)
summ = dict(total=len(results), passed=sum(r["result"] == "PASS" for r in results),
failed=sum(r["result"] == "FAIL" for r in results))
json.dump(dict(summary=summ, results=results), open(RESULT_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
print(json.dumps(summ))
main()