Files
Pier_v1/Code/교각모델링앱/engine/rhino/checks/g1_shaft.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 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>
2026-10-28 11:20:18 +09:00

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