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