- 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>
265 lines
13 KiB
Python
265 lines
13 KiB
Python
"""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()
|