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,110 @@
"""G5 stage: bridge bearings (black boxes) and sole plates (wedges) on each h2 pad.
Run inside Rhino 8 via runner (globals PARAMS_PATH) after concrete.py.
Ported from Code/교각모델링/build_bearing_plates_rhino.py (Ungok P1, model v0.1.09). Rules: 설계서 v0.1.10 §8. Reads params.json `bearing_seats`,
`sole_plates`, `coping`. Bottom of the bearing = modelled pad top (= coping step + h1 + h2 = EL.'X' within 1 mm).
"""
import json
import math
import Rhino
import Rhino.Geometry as rg
import System
from Rhino.DocObjects import ObjectAttributes, ObjectColorSource
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
H3 = P["sole_plates"].get("h3") # D2 ELEVATION TABLE h3 (common value; a table row may override)
def layer(path, color):
parent = -1
parts = path.split("::")
for i in range(1, len(parts) + 1):
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
if idx < 0:
lay = Rhino.DocObjects.Layer()
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
if parent >= 0:
lay.ParentLayerId = doc.Layers[parent].Id
idx = doc.Layers.Add(lay)
parent = idx
return parent
def corners(cx, cy, along, across, rot_deg):
"""Plan corners, `along` on +Y rotated clockwise by rot_deg; order: (-a,-c), (+a,-c), (+a,+c), (-a,+c)."""
t = math.radians(rot_deg)
ua, uc = (math.sin(t), math.cos(t)), (math.cos(t), -math.sin(t))
out = []
for sa, sc in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
out.append((cx + sa * ua[0] * along / 2 + sc * uc[0] * across / 2,
cy + sa * ua[1] * along / 2 + sc * uc[1] * across / 2, sa * along / 2))
return out
def hexa(bottom, top):
pts = [rg.Point3d(*p) for p in bottom + top]
b = rg.Brep.CreateFromBox(pts)
if b is None or not b.IsSolid:
raise RuntimeError("hexahedron failed")
if b.SolidOrientation == rg.BrepSolidOrientation.Inward:
b.Flip()
return b
def add(brep, key, oid, color, props):
attr = ObjectAttributes()
attr.LayerIndex = layer(key, color)
attr.Name = oid
attr.ObjectColor = Color.FromArgb(*color)
attr.ColorSource = ObjectColorSource.ColorFromObject
attr.SetUserString("id", oid)
for k, v in props.items():
attr.SetUserString(k, str(v))
if doc.Objects.AddBrep(brep, attr) == System.Guid.Empty:
raise RuntimeError(f"{oid}: AddBrep failed")
def main():
for o in list(doc.Objects):
if (o.Attributes.GetUserString("id") or "").startswith((f"{PID}-BB", f"{PID}-SP")):
doc.Objects.Delete(o, True)
sp = P["sole_plates"]
if not sp["table"]: # 2026-10-13: an empty table silently produced no bearings
raise RuntimeError("sole_plates.table is empty")
steps = P["coping"].get("top_steps")
bear = {b["no"]: b for b in P["bearing_seats"]["bearings"]}
for r in sp["table"]:
b = bear[r["no"]]
cx, cy = b["centre"]
rot = b["rotation_deg"]
seat = b["seat_z"] if "seat_z" in b else steps[b["girder_line"] - 1]["z"] # seat_z: tapered form (Ulsan)
z0 = seat + b["h1"] + b["h2"] # modelled pad top
h3 = r.get("h3", H3) # per bearing when tabled
if sp.get("bearing_size") == "AB": # bearing body A (along) x B (across) (Ulsan detail drawing)
along, across, rule = b["A"], b["B"], "A x B (교량받침 상세)"
else: # (L-100) x (W-100) per user 2026-10-08 (Ungok)
along, across, rule = r["L"] - sp["bearing_shrink"], r["W"] - sp["bearing_shrink"], None
cb = corners(cx, cy, along, across, rot)
add(hexa([(x, y, z0) for x, y, _ in cb], [(x, y, z0 + h3) for x, y, _ in cb]),
f"{PID}::교량받침", f"{PID}-BB{r['no']:02d}", tuple(sp["colours"]["bearing"]),
dict(role="bridge bearing", no=r["no"], size=f"{along}x{across}x{h3}", el_x=b["el_x"],
src=f"ELEVATION TABLE h3, {rule}" if rule else "D2 ELEVATION TABLE h3, (L-100)x(W-100) per user 2026-10-08"))
# sole plate: bottom flat, top sloped S% along the pad long axis, centre thickness T (sign of S sets thick end)
zb = z0 + h3
cs = corners(cx, cy, r["L"], r["W"], rot)
top = [(x, y, zb + r["T"] + r["S"] / 100 * s) for x, y, s in cs]
add(hexa([(x, y, zb) for x, y, _ in cs], top), f"{PID}::솔플레이트", f"{PID}-SP{r['no']:02d}",
tuple(sp["colours"]["sole_plate"]),
dict(role="sole plate", no=r["no"], S=r["S"], L=r["L"], W=r["W"], T=r["T"],
end_thick=f"{r['T'] - r['S'] / 100 * r['L'] / 2:.2f}/{r['T'] + r['S'] / 100 * r['L'] / 2:.2f}",
src=sp.get("sole_plate_src", "D2 솔플레이트 상세 TABLE; slope along pad axis, thick end -Y [HITL]")))
doc.Views.Redraw()
print(json.dumps({"bearings": len(sp["table"]), "sole_plates": len(sp["table"])}))
main()
@@ -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()
@@ -0,0 +1,180 @@
"""G2 stage: column rebar P1-P6 (centreline + pipe solid) from params `column_rebar`.
Run inside Rhino 8 via runner (globals PARAMS_PATH) after concrete.py.
Ported from Code/교각모델링/build_rebar_rhino.py (Ungok P1, model v0.1.09). Rules: 설계서 v0.1.04 §5 (HITL H1-H3 approved 2026-10-07). Units mm, Z = EL x 1000.
Each centreline carries `design_length` (schedule-style original length: straight + arc lengths
before hook fillets) and the actual curve length is measured separately (box rule: 원길이 vs 곡선길이).
"""
import json
import math
import Rhino
import Rhino.Geometry as rg
from Rhino.DocObjects import ObjectAttributes
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
MARK_DIA = {m: v["dia"] for m, v in P["rebar_marks"].items()}
DIA = {d: float(d[1:]) for d in MARK_DIA.values()} # 'S32' -> 32.0
COLORS = {m: tuple(c) for m, c in P["display"]["column_mark_colours"].items()}
CL_RGB = tuple(P["display"]["centreline_colour"])
Z_FT_TOP = P["footing"]["z_bottom"] + P["footing"]["thickness"] # ties/hoops below -> anchored in footing
Z_CP_BOT = P["coping"]["z_bottom_flat"] # above -> anchored in coping
def layer(path, color):
idx = doc.Layers.FindByFullPath(path, -1)
if idx >= 0:
return idx
parent = -1
parts = path.split("::")
for i in range(1, len(parts) + 1):
sub = "::".join(parts[:i])
idx = doc.Layers.FindByFullPath(sub, -1)
if idx < 0:
lay = Rhino.DocObjects.Layer()
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
if parent >= 0:
lay.ParentLayerId = doc.Layers[parent].Id
idx = doc.Layers.Add(lay)
parent = idx
return parent
def min_bend_radius(dia_name):
d = DIA[dia_name]
inner = 2 * d if d <= 16 else 3.5 * d # box project rule (REBAR_BENDS_20260914)
return (inner + d) / 2
def add_bar(curve, mark, occ, design_len, hosts, src, coupler=0.0):
d = DIA[MARK_DIA[mark]]
# fitRail=False: fitRail=True fails on line-arc-line rails and crashed a headless Rhino (2026-10-07)
pipes = rg.Brep.CreatePipe(curve, d / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
if not pipes or not pipes[0].IsSolid:
raise RuntimeError(f"{mark} {occ}: pipe failed")
common = dict(mark=mark, dia=MARK_DIA[mark], shape=P["column_rebar"]["schedule"][mark]["shape"],
occurrence=occ, host=f"{PID}-CL", anchor_hosts=hosts, design_length=f"{design_len:.3f}",
coupler_length=f"{coupler:.3f}",
src=src)
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
(pipes[0], layer(f"{PID}::철근::기둥::{mark}", COLORS[mark]), f"{PID}-R")):
attr = ObjectAttributes()
attr.LayerIndex = lay
oid = f"{prefix}-{mark}-{occ}"
attr.Name = oid
attr.SetUserString("id", oid)
for k, v in common.items():
attr.SetUserString(k, v)
if isinstance(geo, rg.Brep):
doc.Objects.AddBrep(geo, attr)
else:
doc.Objects.AddCurve(geo, attr)
def pt(r, deg, z):
t = math.radians(deg)
return rg.Point3d(r * math.cos(t), r * math.sin(t), z)
def arc_xy(r, a0, a1, z):
"""Horizontal arc at height z from angle a0 to a1 (deg, counter-clockwise)."""
am = (a0 + a1) / 2
return rg.ArcCurve(rg.Arc(pt(r, a0, z), pt(r, am, z), pt(r, a1, z)))
def hooked_half_ring(r, a0, z, hook, bend_r):
"""Semicircle a0 -> a0+180 with inward 135-degree hooks at both ends (P3, shape VI)."""
arc = arc_xy(r, a0, a0 + 180, z)
pieces = []
for at_start in (True, False):
p = arc.PointAtStart if at_start else arc.PointAtEnd
t = arc.TangentAtStart if at_start else arc.TangentAtEnd
if at_start:
t = -t # travel direction away from the arc
n = rg.Vector3d(-p.X, -p.Y, 0)
n.Unitize()
d = t * math.cos(math.radians(135)) + n * math.sin(math.radians(135)) # back + inward
d.Unitize()
pieces.append(rg.LineCurve(p, p + d * hook))
joined = rg.Curve.JoinCurves([pieces[0], arc, pieces[1]], TOL)[0]
fil = rg.Curve.CreateFilletCornersCurve(joined, bend_r, TOL, math.radians(1))
return fil or joined
def main():
for o in list(doc.Objects):
if (o.Attributes.GetUserString("id") or "").startswith((f"{PID}-R-", f"{PID}-RC-")):
doc.Objects.Delete(o, True)
cr = P["column_rebar"]
mb, hp, tie = cr["main_bars"], cr["hoops"], cr["ties"]
sch = cr["schedule"]
count = 0
# Main bars P1 (outer, hooked) / P2 (inner, straight)
p1, p2 = mb["p1"], mb["p2"]
for i in range(mb["divisions"]):
th = mb["start_deg"] * 0 + i * mb["pitch_deg"] # start angle 0 (CAD 0.001 deg)
ro, ri = mb["r_outer"], mb["r_inner"]
z_leg, R = p1["hook_leg_z"], p1["bend_r"]
top = p1["straight_from"] + p1["straight_len"]
leg = rg.LineCurve(pt(ro + R + p1["hook_leg"], th, z_leg), pt(ro + R, th, z_leg))
bend = rg.ArcCurve(rg.Arc(pt(ro + R, th, z_leg),
pt(ro + R - R * math.sin(math.radians(45)), th,
z_leg + R - R * math.cos(math.radians(45))),
pt(ro, th, z_leg + R)))
vert = rg.LineCurve(pt(ro, th, z_leg + R), pt(ro, th, top))
c1 = rg.Curve.JoinCurves([leg, bend, vert], TOL)[0]
add_bar(c1, "P1", f"{i + 1:02d}", p1["straight_len"] + math.pi / 2 * R + p1["hook_leg"],
f"{PID}-FT,{PID}-CP", f"angle {th:.4f} deg; D1 단면 C-C / 정면도; D3 P1")
c2 = rg.LineCurve(pt(ri, th, p2["z0"]), pt(ri, th, p2["z0"] + p2["length"]))
add_bar(c2, "P2", f"{i + 1:02d}", p2["length"], f"{PID}-FT,{PID}-CP",
f"angle {th:.4f} deg; D1 단면 C-C / 정면도; D3 P2")
count += 2
# Hoops: P4 (V: 2 half arcs + 2 coupler gaps), P5 (IX: ring + 1 gap), P3 (VI: 2 hooked halves)
r = hp["radius"]
n3 = 0
for k, (z, mark) in enumerate(zip(hp["levels"], hp["marks"])):
r = hp.get("radius_by_mark", {}).get(mark, hp["radius"]) # Ungok P2: P3 R899, P4/P5 R904
s = sch[mark]
a = float(s["A"]) * 1000 if float(s["A"]) < 100 else float(s["A"])
if mark in ("P4", "P5"):
gap = float(s["B"])
gap_deg = math.degrees(gap / r)
arcs = ([(90 + gap_deg / 2, 270 - gap_deg / 2), (270 + gap_deg / 2, 450 - gap_deg / 2)]
if mark == "P4" else [(90 + gap_deg / 2, 450 - gap_deg / 2)])
curves = [arc_xy(r, a0, a1, z) for a0, a1 in arcs]
for j, c in enumerate(curves):
add_bar(c, mark, f"L{k + 1:02d}{'ab'[j] if len(curves) > 1 else ''}", a, "",
f"EL {z / 1000:.3f}; coupler gap {gap:.0f} at {'90/270' if mark == 'P4' else '90'} deg",
coupler=gap) # one coupler (b) per piece: P4 2 pieces/2 gaps, P5 1/1
else:
n3 += 1
a0 = 90 if n3 % 2 else 0 # joins staggered by 90 deg
hook = float(s["B"])
for j, start in enumerate((a0, a0 + 180)):
c = hooked_half_ring(r, start, z, hook, min_bend_radius(MARK_DIA["P3"]))
add_bar(c, mark, f"L{k + 1:02d}{'ab'[j]}", a + 2 * hook,
f"{PID}-CP" if z > Z_CP_BOT else "", f"EL {z / 1000:.3f}; joins at {a0}/{a0 + 180} deg")
count += 1
# Cross ties P6 (III): straight 1836 on diameters, 150 hooks down at r=918, sitting on hoop
half = tie["straight"] / 2
zoff = DIA[MARK_DIA["P4"]] / 2 + DIA[MARK_DIA["P6"]] / 2
for k, z in enumerate(tie["levels"]):
for ang in tie["angles_deg"]:
zt = z + zoff
pts = [pt(half, ang + 180, zt - tie["hook"]), pt(half, ang + 180, zt), pt(half, ang, zt),
pt(half, ang, zt - tie["hook"])]
pl = rg.PolylineCurve(pts)
c = rg.Curve.CreateFilletCornersCurve(pl, min_bend_radius(MARK_DIA["P6"]), TOL, math.radians(1)) or pl
add_bar(c, "P6", f"L{k + 1}-{ang:03d}", tie["straight"] + 2 * tie["hook"],
f"{PID}-FT" if z < Z_FT_TOP else (f"{PID}-CP" if z > Z_CP_BOT else ""),
f"EL {z / 1000:.3f} (+{zoff} on hoop); angle {ang} deg")
count += 1
doc.Views.Redraw()
print(json.dumps({"bars": count}))
main()
@@ -0,0 +1,205 @@
"""G2 stage, form "ring" (Ulsan P1, 2026-10-13): circular column rebar with one main-bar ring, hoop zones of
different hoop types and cross ties, from params `column_rebar` (centreline + pipe solid per bar).
Run inside Rhino 8 via runner (globals PARAMS_PATH) after the G1 builder. Units mm, Z = EL x 1000.
params column_rebar:
main {mark, count, start_deg, radius, hook_leg_z, bend_r, hook_leg, straight_from, straight_len}
bar = horizontal leg pointing outwards at hook_leg_z, R bend, vertical straight part (shape VI)
hoops {radius, zones: [{mark, type, levels, a, gap | lap+hook}]}
type "coupler_half": two half arcs, coupler gaps `gap` at 90 / 270 deg (shape V)
"coupler_ring": one ring, one coupler gap at 90 deg (shape X)
"lap_half": two half rings, each extended by lap/2 at both ends, 135-deg hooks (shape VI);
joins alternate between 90/270 and 0/180 deg
ties {mark, levels, angles_deg, straight, hook, z_offset}: straight bars on diameters, hooks down
`design_length` is the schedule-style original length; `coupler_length` the gap per piece (schedule L
includes the coupler).
Optional (Okseong P1, 2026-10-14; absent -> unchanged): main.inner {mark, radius} second ring at the same angles
with the hook bent inwards; zone.radius per hoop zone; ties.shape "90-135" {leg, tail, y_lift, design_length}:
straight on the X / Y diameters, 90-deg leg down at one end, 135-deg hook at the other, ends swapped per level,
the Y tie y_lift above the X tie.
"""
import json
import math
import Rhino
import Rhino.Geometry as rg
from Rhino.DocObjects import ObjectAttributes
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items()}
DIA = {d: float(d[1:]) for d, _ in MARK.values()}
CL_RGB = tuple(P["display"]["centreline_colour"])
COLORS = {m: tuple(c) for m, c in P["display"].get("column_mark_colours", {}).items()}
Z_FT_TOP = P["footing"]["z_bottom"] + P["footing"]["thickness"]
Z_CP_BOT = Z_FT_TOP + P["column"]["height"]
def layer(path, color):
parent = -1
parts = path.split("::")
for i in range(1, len(parts) + 1):
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
if idx < 0:
lay = Rhino.DocObjects.Layer()
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
if parent >= 0:
lay.ParentLayerId = doc.Layers[parent].Id
idx = doc.Layers.Add(lay)
parent = idx
return parent
def min_bend_radius(dia_name):
d = DIA[dia_name]
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2
def hosts(z):
return f"{PID}-FT" if z < Z_FT_TOP else (f"{PID}-CP" if z > Z_CP_BOT else "")
def add_bar(curve, mark, occ, design_len, anchor, src, coupler=0.0):
dia, shape = MARK[mark]
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
if not pipes or not pipes[0].IsSolid:
raise RuntimeError(f"{mark} {occ}: pipe failed")
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-CL", anchor_hosts=anchor,
design_length=f"{design_len:.3f}", coupler_length=f"{coupler:.3f}", src=src)
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
(pipes[0], layer(f"{PID}::철근::기둥::{mark}", COLORS.get(mark, (200, 140, 90))), f"{PID}-R")):
attr = ObjectAttributes()
attr.LayerIndex = lay
oid = f"{prefix}-{mark}-{occ}"
attr.Name = oid
attr.SetUserString("id", oid)
for k, v in common.items():
attr.SetUserString(k, v)
gid = doc.Objects.AddBrep(geo, attr) if isinstance(geo, rg.Brep) else doc.Objects.AddCurve(geo, attr)
if gid == __import__("System").Guid.Empty:
raise RuntimeError(f"{oid}: add failed")
def pt(r, deg, z):
t = math.radians(deg)
return rg.Point3d(r * math.cos(t), r * math.sin(t), z)
def arc_xy(r, a0, a1, z):
return rg.ArcCurve(rg.Arc(pt(r, a0, z), pt(r, (a0 + a1) / 2, z), pt(r, a1, z)))
def hooked_arc(r, a0, a1, z, hook, bend_r):
"""Arc a0 -> a1 (deg, ccw) with inward 135-deg hooks at both ends, fillet bend_r."""
arc = arc_xy(r, a0, a1, z)
pieces = []
for at_start in (True, False):
p = arc.PointAtStart if at_start else arc.PointAtEnd
t = -arc.TangentAtStart if at_start else arc.TangentAtEnd
n = rg.Vector3d(-p.X, -p.Y, 0)
n.Unitize()
d = t * math.cos(math.radians(135)) + n * math.sin(math.radians(135))
d.Unitize()
pieces.append(rg.LineCurve(p, p + d * hook))
joined = rg.Curve.JoinCurves([pieces[0], arc, pieces[1]], TOL)[0]
return rg.Curve.CreateFilletCornersCurve(joined, bend_r, TOL, math.radians(1)) or joined
def main():
for o in list(doc.Objects):
oid = o.Attributes.GetUserString("id") or ""
if oid.startswith((f"{PID}-R-P", f"{PID}-RC-P")):
doc.Objects.Delete(o, True)
cr = P["column_rebar"]
m = cr["main"]
n = 0
ro, R = m["radius"], m["bend_r"]
top = m["straight_from"] + m["straight_len"]
for i in range(m["count"]):
th = m["start_deg"] + i * 360.0 / m["count"]
leg = rg.LineCurve(pt(ro + R + m["hook_leg"], th, m["hook_leg_z"]), pt(ro + R, th, m["hook_leg_z"]))
bend = rg.ArcCurve(rg.Arc(pt(ro + R, th, m["hook_leg_z"]),
pt(ro + R - R * math.sin(math.radians(45)), th, m["hook_leg_z"] + R - R * math.cos(math.radians(45))),
pt(ro, th, m["hook_leg_z"] + R)))
vert = rg.LineCurve(pt(ro, th, m["hook_leg_z"] + R), pt(ro, th, top))
c = rg.Curve.JoinCurves([leg, bend, vert], TOL)[0]
add_bar(c, m["mark"], f"{i + 1:02d}", m["straight_len"] + math.pi / 2 * R + m["hook_leg"],
f"{PID}-FT,{PID}-CP", f"angle {th:.4f} deg; 배근도(1) 정면도 갈고리 R{R:.0f}·{m['hook_leg']:.0f}; 단면 DIVISION {m['count']}")
n += 1
if "inner" in m: # second ring, hook bent inwards (Okseong P1)
ri = m["inner"]["radius"]
z0 = m["hook_leg_z"]
leg = rg.LineCurve(pt(ri - R - m["hook_leg"], th, z0), pt(ri - R, th, z0))
bend = rg.ArcCurve(rg.Arc(pt(ri - R, th, z0),
pt(ri - R + R * math.sin(math.radians(45)), th, z0 + R - R * math.cos(math.radians(45))),
pt(ri, th, z0 + R)))
vert = rg.LineCurve(pt(ri, th, z0 + R), pt(ri, th, top))
c = rg.Curve.JoinCurves([leg, bend, vert], TOL)[0]
add_bar(c, m["inner"]["mark"], f"{i + 1:02d}", m["straight_len"] + math.pi / 2 * R + m["hook_leg"],
f"{PID}-FT,{PID}-CP", f"angle {th:.4f} deg; inner ring r {ri:.0f}, hook inwards")
n += 1
hp = cr["hoops"]
r = hp["radius"]
k = 0
for zone in hp["zones"]:
mark, typ = zone["mark"], zone["type"]
r = zone.get("radius", hp["radius"]) # per zone (Okseong P3 1,150 / P4 1,147)
for j, z in enumerate(zone["levels"]):
k += 1
if typ == "coupler_half":
gd = math.degrees(zone["gap"] / r)
arcs = [(90 + gd / 2, 270 - gd / 2), (270 + gd / 2, 450 - gd / 2)]
for q, (a0, a1) in enumerate(arcs):
add_bar(arc_xy(r, a0, a1, z), mark, f"L{k:03d}{'ab'[q]}", zone["a"], hosts(z),
f"EL {z / 1000:.3f}; coupler gaps {zone['gap']:.0f} at 90/270 deg [추론]", coupler=zone["gap"])
elif typ == "coupler_ring":
gd = math.degrees(zone["gap"] / r)
add_bar(arc_xy(r, 90 + gd / 2, 450 - gd / 2, z), mark, f"L{k:03d}", zone["a"], hosts(z),
f"EL {z / 1000:.3f}; coupler gap {zone['gap']:.0f} at 90 deg [추론]", coupler=zone["gap"])
elif typ == "lap_half":
ext = math.degrees(zone["lap"] / 2 / r)
a0 = 90 if j % 2 == 0 else 0 # joins alternate by 90 deg [추론]
for q, s in enumerate((a0, a0 + 180)):
c = hooked_arc(r, s - ext, s + 180 + ext, z, zone["hook"], min_bend_radius(MARK[mark][0]))
add_bar(c, mark, f"L{k:03d}{'ab'[q]}", zone["a"] + zone["lap"] + 2 * zone["hook"], hosts(z),
f"EL {z / 1000:.3f}; laps {zone['lap']:.0f} at {a0}/{a0 + 180} deg [추론]")
else:
raise RuntimeError(f"unknown hoop type {typ}")
n += 1
tie = cr["ties"]
if tie.get("shape") == "90-135": # Okseong P1 (2026-10-14), as column_rebar_straight.py
rb = min_bend_radius(MARK[tie["mark"]][0])
for q, z in enumerate(tie["levels"]):
for axis, name in ((0, "X"), (1, "Y")):
zt = z + (tie["y_lift"] if axis else 0)
u = rg.Vector3d(1, 0, 0) if axis == 0 else rg.Vector3d(0, 1, 0)
s = -1 if q % 2 else 1
a = rg.Point3d(0, 0, zt) - u * (s * tie["straight"] / 2)
b = rg.Point3d(0, 0, zt) + u * (s * tie["straight"] / 2)
d = -u * s * math.cos(math.radians(45)) + rg.Vector3d(0, 0, -1) * math.sin(math.radians(45))
pl = rg.PolylineCurve([a + rg.Vector3d(0, 0, -tie["leg"]), a, b, b + d * tie["tail"]])
c = rg.Curve.CreateFilletCornersCurve(pl, rb, TOL, math.radians(1)) or pl
add_bar(c, tie["mark"], f"L{q + 1:02d}{name}", tie["design_length"], hosts(zt),
f"EL {zt / 1000:.3f}; {name} (Y +{tie['y_lift']}, 90/135 교번 [추론])")
n += 1
doc.Views.Redraw()
print(json.dumps({"bars": n}))
return
half = tie["straight"] / 2
for q, z in enumerate(tie["levels"]):
zt = z + tie["z_offset"]
for ang in tie["angles_deg"]:
pts = [pt(half, ang + 180, zt - tie["hook"]), pt(half, ang + 180, zt), pt(half, ang, zt), pt(half, ang, zt - tie["hook"])]
pl = rg.PolylineCurve(pts)
c = rg.Curve.CreateFilletCornersCurve(pl, min_bend_radius(MARK[tie["mark"]][0]), TOL, math.radians(1)) or pl
add_bar(c, tie["mark"], f"L{q + 1:02d}-{ang:05.1f}", tie["straight"] + 2 * tie["hook"], hosts(z),
f"EL {z / 1000:.3f} (+{tie['z_offset']} on hoop); angle {ang:.2f} deg")
n += 1
doc.Views.Redraw()
print(json.dumps({"bars": n}))
main()
@@ -0,0 +1,150 @@
"""G2 stage, form "straight" (Ramp-A P1, 2026-10-13): column on a drilled shaft, no footing. Straight main bars
in one ring (bottom coupler to the shaft cage, not modelled as a solid), lapped half-ring hoops with 135-deg hooks,
cross ties in X and Y (90-deg leg at one end, 135-deg hook at the other), from params `column_rebar`.
Run inside Rhino 8 via runner (globals PARAMS_PATH) after the G1 builder. Units mm, Z = EL x 1000.
params column_rebar:
main {mark, count, start_deg, radius, z0, z1, coupler_length}
hoops {mark, radius, levels, a, lap, hook, join_deg [first, second]} joins alternate per level (K4)
ties {mark, levels, straight, leg, hook_tail, y_lift} X tie at the level, Y tie y_lift above (K3);
the 90-deg leg (down) and the 135-deg hook swap ends per level (K4)
design lengths {mark: schedule-style original length per piece}
Hosts for cover / anchorage: below the column bottom = pile top segment, above the column top = coping.
"""
import json
import math
import Rhino
import Rhino.Geometry as rg
from Rhino.DocObjects import ObjectAttributes
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items()}
DIA = {d: float(d[1:]) for d, _ in MARK.values()}
CL_RGB = tuple(P["display"]["centreline_colour"])
COLORS = {m: tuple(c) for m, c in P["display"].get("column_mark_colours", {}).items()}
Z_CL0, Z_CL1 = P["column"]["z0"], P["column"]["z1"]
PILE_TOP = f"{PID}-PL{len(P['pile']['segments']):02d}"
def layer(path, color):
parent = -1
parts = path.split("::")
for i in range(1, len(parts) + 1):
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
if idx < 0:
lay = Rhino.DocObjects.Layer()
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
if parent >= 0:
lay.ParentLayerId = doc.Layers[parent].Id
idx = doc.Layers.Add(lay)
parent = idx
return parent
def min_bend_radius(dia_name):
d = DIA[dia_name]
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2
def hosts(z):
return PILE_TOP if z < Z_CL0 else (f"{PID}-CP" if z > Z_CL1 else "")
def add_bar(curve, mark, occ, design_len, anchor, src, coupler=0.0):
dia, shape = MARK[mark]
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
if not pipes or not pipes[0].IsSolid:
raise RuntimeError(f"{mark} {occ}: pipe failed")
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-CL", anchor_hosts=anchor,
design_length=f"{design_len:.3f}", coupler_length=f"{coupler:.3f}", src=src)
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
(pipes[0], layer(f"{PID}::철근::기둥::{mark}", COLORS.get(mark, (200, 140, 90))), f"{PID}-R")):
attr = ObjectAttributes()
attr.LayerIndex = lay
oid = f"{prefix}-{mark}-{occ}"
attr.Name = oid
attr.SetUserString("id", oid)
for k, v in common.items():
attr.SetUserString(k, v)
gid = doc.Objects.AddBrep(geo, attr) if isinstance(geo, rg.Brep) else doc.Objects.AddCurve(geo, attr)
if gid == __import__("System").Guid.Empty:
raise RuntimeError(f"{oid}: add failed")
def pt(r, deg, z):
t = math.radians(deg)
return rg.Point3d(r * math.cos(t), r * math.sin(t), z)
def hooked_arc(r, a0, a1, z, hook, bend_r):
"""Arc a0 -> a1 (deg, ccw) with inward 135-deg hooks at both ends, fillet bend_r (as column_rebar_ring.py)."""
arc = rg.ArcCurve(rg.Arc(pt(r, a0, z), pt(r, (a0 + a1) / 2, z), pt(r, a1, z)))
pieces = []
for at_start in (True, False):
p = arc.PointAtStart if at_start else arc.PointAtEnd
t = -arc.TangentAtStart if at_start else arc.TangentAtEnd
n = rg.Vector3d(-p.X, -p.Y, 0)
n.Unitize()
d = t * math.cos(math.radians(135)) + n * math.sin(math.radians(135))
d.Unitize()
pieces.append(rg.LineCurve(p, p + d * hook))
joined = rg.Curve.JoinCurves([pieces[0], arc, pieces[1]], TOL)[0]
return rg.Curve.CreateFilletCornersCurve(joined, bend_r, TOL, math.radians(1)) or joined
def tie_curve(axis, z, half, leg, tail, flip):
"""Straight part on the X (axis 0) or Y (axis 1) diameter at z; 90-deg leg `leg` down at one end, 135-deg hook
(tail pointing down and back inwards) at the other; `flip` swaps the ends."""
u = rg.Vector3d(1, 0, 0) if axis == 0 else rg.Vector3d(0, 1, 0)
s = -1 if flip else 1
a, b = rg.Point3d(0, 0, z) - u * (s * half), rg.Point3d(0, 0, z) + u * (s * half)
d = -u * s * math.cos(math.radians(45)) + rg.Vector3d(0, 0, -1) * math.sin(math.radians(45))
return rg.PolylineCurve([a + rg.Vector3d(0, 0, -leg), a, b, b + d * tail])
def main():
for o in list(doc.Objects):
oid = o.Attributes.GetUserString("id") or ""
if oid.startswith((f"{PID}-R-P", f"{PID}-RC-P")):
doc.Objects.Delete(o, True)
cr = P["column_rebar"]
DL = cr["design_lengths"]
m = cr["main"]
n = 0
for i in range(m["count"]):
th = m["start_deg"] + i * 360.0 / m["count"]
c = rg.LineCurve(pt(m["radius"], th, m["z0"]), pt(m["radius"], th, m["z1"]))
add_bar(c, m["mark"], f"{i + 1:02d}", DL[m["mark"]], f"{PILE_TOP},{PID}-CP",
f"angle {th:.4f} deg; D2 단면 C-C DIVISION {m['count']} r {m['radius']:.0f}; 정면 EL {m['z0'] / 1000:.3f}"
f"~{m['z1'] / 1000:.3f}; 하단 커플러 {m['coupler_length']:.0f} (K1)", coupler=m["coupler_length"])
n += 1
h = cr["hoops"]
r, ext = h["radius"], math.degrees(h["lap"] / 2 / h["radius"])
for j, z in enumerate(h["levels"]):
a0 = h["join_deg"][j % 2]
for q, s in enumerate((a0, a0 + 180)):
c = hooked_arc(r, s - ext, s + 180 + ext, z, h["hook"], min_bend_radius(MARK[h["mark"]][0]))
add_bar(c, h["mark"], f"L{j + 1:03d}{'ab'[q]}", DL[h["mark"]], hosts(z),
f"EL {z / 1000:.3f}; 겹이음 {h['lap']:.0f} at {a0}/{a0 + 180} deg (K4 [추론])")
n += 1
t = cr["ties"]
rb = min_bend_radius(MARK[t["mark"]][0])
for j, z in enumerate(t["levels"]):
for axis, name in ((0, "X"), (1, "Y")):
zt = z + (t["y_lift"] if axis == 1 else 0)
pl = tie_curve(axis, zt, t["straight"] / 2, t["leg"], t["hook_tail"], flip=(j % 2 == 1))
c = rg.Curve.CreateFilletCornersCurve(pl, rb, TOL, math.radians(1)) or pl
add_bar(c, t["mark"], f"L{j + 1:02d}{name}", DL[t["mark"]], hosts(zt),
f"EL {zt / 1000:.3f}; {name} 방향{' +' + str(t['y_lift']) + ' (K3 [추론])' if axis else ''}; "
f"90° 다리·135° 갈고리 {'좌우 교번' if j % 2 else '기본'} (K4 [추론])")
n += 1
doc.Views.Redraw()
print(json.dumps({"bars": n}))
main()
@@ -0,0 +1,190 @@
"""G1 stage: T-pier concrete (lean, footing, circular column, coping, piles, pedestals, pads).
Run inside Rhino 8 via runner with globals PARAMS_PATH (app schema pier-T/1).
Only params are read; no drawing-name branching. Units: mm, Z = EL x 1000.
Ported from Code/교각모델링/build_rhino.py (Ungok P1, model v0.1.09).
"""
import json
import math
import Rhino.Geometry as rg
from Rhino.DocObjects import ObjectAttributes
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
FCK = P["materials"]["fck"]
LAYERS = {k: (f"{PID}::{name}", Color.FromArgb(*rgb)) for k, (name, rgb) in P["display"]["concrete_layers"].items()}
def layer_index(full_path, color):
idx = doc.Layers.FindByFullPath(full_path, -1)
if idx >= 0:
return idx
parent_name, child = full_path.split("::")
parent = doc.Layers.FindByFullPath(parent_name, -1)
if parent < 0:
parent = doc.Layers.Add(parent_name, Color.Black)
layer = __import__("Rhino").DocObjects.Layer()
layer.Name, layer.Color = child, color
layer.ParentLayerId = doc.Layers[parent].Id
return doc.Layers.Add(layer)
def add(brep, key, oid, role, fck, views):
if brep is None or not brep.IsSolid:
raise RuntimeError(f"{oid}: not a closed solid")
attr = ObjectAttributes()
attr.LayerIndex = layer_index(*LAYERS[key])
attr.Name = oid
for k, v in (("id", oid), ("role", role), ("fck", fck), ("source_views", views)):
attr.SetUserString(k, v)
return doc.Objects.AddBrep(brep, attr)
def box(x0, x1, y0, y1, z0, z1):
return rg.Box(rg.BoundingBox(x0, y0, z0, x1, y1, z1)).ToBrep()
def cylinder(x, y, z0, z1, r):
circle = rg.Circle(rg.Plane(rg.Point3d(x, y, z0), rg.Vector3d.ZAxis), r)
return rg.Cylinder(circle, z1 - z0).ToBrep(True, True)
def arc_through(p_start, p_end, radius, y):
"""Arc of given radius between two XZ points; center on the lower side (soffit)."""
(x1, z1), (x2, z2) = p_start, p_end
mx, mz = (x1 + x2) / 2, (z1 + z2) / 2
dx, dz = x2 - x1, z2 - z1
chord = math.hypot(dx, dz)
h = math.sqrt(radius ** 2 - (chord / 2) ** 2)
nx, nz = -dz / chord, dx / chord
centers = [(mx + nx * h, mz + nz * h), (mx - nx * h, mz - nz * h)]
cx, cz = min(centers, key=lambda c: c[1])
a1, a2 = math.atan2(z1 - cz, x1 - cx), math.atan2(z2 - cz, x2 - cx)
am = (a1 + a2) / 2
mid = rg.Point3d(cx + radius * math.cos(am), y, cz + radius * math.sin(am))
arc = rg.Arc(rg.Point3d(x1, y, z1), mid, rg.Point3d(x2, y, z2))
return rg.ArcCurve(arc), (cx, cz)
def coping_brep(c):
y0 = -c["width_y"] / 2
half = c["length_bottom_tip_to_tip"] / 2
zb, rise, r = c["z_bottom_flat"], c["tip_bottom_rise"], c["soffit_radius"]
flat, inset = c["flat_bottom_half_width"], c["end_batter_top_inset"]
steps = c["top_steps"]
right_arc, rc = arc_through((flat, zb), (half, zb + rise), r, y0)
left_arc, lc = arc_through((-half, zb + rise), (-flat, zb), r, y0)
pts = [(half, zb + rise), (half - inset, steps[-1]["z"])]
for s_prev, s in zip(reversed(steps[1:]), reversed(steps[:-1])):
pts += [(s_prev["x0"], s_prev["z"]), (s["x1"], s["z"])]
pts += [(-half + inset, steps[0]["z"]), (-half, zb + rise)]
top = rg.PolylineCurve([rg.Point3d(x, y0, z) for x, z in pts])
bottom = rg.LineCurve(rg.Point3d(-flat, y0, zb), rg.Point3d(flat, y0, zb))
joined = rg.Curve.JoinCurves([bottom, right_arc, top, left_arc], TOL)
if len(joined) != 1 or not joined[0].IsClosed:
raise RuntimeError("coping profile not closed")
srf = rg.Surface.CreateExtrusion(joined[0], rg.Vector3d(0, c["width_y"], 0))
brep = srf.ToBrep().CapPlanarHoles(TOL)
if brep.SolidOrientation == rg.BrepSolidOrientation.Inward:
brep.Flip()
return brep, {"right_arc_center": rc, "left_arc_center": lc}
def prism(pts_xy, z0, z1):
poly = rg.PolylineCurve([rg.Point3d(x, y, z0) for x, y in pts_xy + pts_xy[:1]])
srf = rg.Surface.CreateExtrusion(poly, rg.Vector3d(0, 0, z1 - z0))
b = srf.ToBrep().CapPlanarHoles(TOL)
if b is not None and b.SolidOrientation == rg.BrepSolidOrientation.Inward:
b.Flip()
return b
def rotated_rect(cx, cy, along, across, rot_deg, z):
"""Rectangle with `along` side on +Y rotated clockwise by rot_deg (plan view)."""
t = math.radians(rot_deg)
ua, uc = (math.sin(t), math.cos(t)), (math.cos(t), -math.sin(t))
pts = [(cx + sa * ua[0] * along / 2 + sc * uc[0] * across / 2,
cy + sa * ua[1] * along / 2 + sc * uc[1] * across / 2)
for sa, sc in ((-1, -1), (1, -1), (1, 1), (-1, 1))]
return rg.PolylineCurve([rg.Point3d(x, y, z) for x, y in pts + pts[:1]])
def frustum(bottom, top):
lofts = rg.Brep.CreateFromLoft([bottom, top], rg.Point3d.Unset, rg.Point3d.Unset,
rg.LoftType.Straight, False)
b = lofts[0].CapPlanarHoles(TOL)
if b is not None and b.SolidOrientation == rg.BrepSolidOrientation.Inward:
b.Flip()
return b
def add_bearing_seats(bs, steps):
"""Pedestals (h1) per girder line split front/rear, and h2 pads (D2 TABLE A, B, h2)."""
ped = bs["pedestal_girder5"]
ref = bs["reference_girder"]
mb, mt = bs["pad_margin_bottom"], bs["pad_margin_top"]
by_no = {b["no"]: b for b in bs["bearings"]}
for b in bs["bearings"]:
shift = (b["girder_line"] - ref) * bs["pitch_x"]
half = ped["front_half"] if b["row"] == "front" else ped["rear_half"]
z0 = steps[b["girder_line"] - 1]["z"]
z1 = z0 + b["h1"]
tag = " [추론: 평행 이동]" if b["inferred"] else ""
add(prism([(x + shift, y) for x, y in half], z0, z1), "seat", f"{PID}-BS{b['no']:02d}",
f"bearing pedestal h1 NO.{b['no']} girder {b['girder_line']} {b['row']}{tag}", FCK["seat"],
"D1 평면도, D1 단면도, D2 ELEVATION TABLE")
cx, cy = b["centre"]
bottom = rotated_rect(cx, cy, b["A"] + mb, b["B"] + mb, b["rotation_deg"], z1)
top = rotated_rect(cx, cy, b["A"] + mt, b["B"] + mt, b["rotation_deg"], z1 + b["h2"])
add(frustum(bottom, top), "pad", f"{PID}-BP{b['no']:02d}",
f"bearing pad h2 NO.{b['no']} {b['type']}{tag}", "-",
"D1 평면도, D1 단면도, D2 교량받침 상세·ELEVATION TABLE")
return len(by_no) * 2
def main():
# Rebuild from scratch: remove previous P1 objects only.
for o in list(doc.Objects):
if o.Attributes.GetUserString("id"):
doc.Objects.Delete(o, True)
lc, ft, cl, cp, pl = (P[k] for k in ("lean_concrete", "footing", "column", "coping", "piles"))
ft_top = ft["z_bottom"] + ft["thickness"]
col_top = ft_top + cl["height"]
if abs(col_top - cp["z_bottom_flat"]) > TOL:
raise RuntimeError(f"column top {col_top} != coping bottom {cp['z_bottom_flat']}")
pile_top = ft["z_bottom"] + pl["head_embedment"]
px, py = pl.get("pitch_x", pl.get("pitch")), pl.get("pitch_y", pl.get("pitch")) # Ungok P2: X 1750 / Y 1550
pile_xy = [(-((pl["count_x"] - 1) * px) / 2 + i * px,
-((pl["count_y"] - 1) * py) / 2 + j * py)
for j in range(pl["count_y"]) for i in range(pl["count_x"])]
piles = [cylinder(x, y, pile_top - pl["length"], pile_top, pl["diameter"] / 2) for x, y in pile_xy]
lean = box(-lc["size_x"] / 2, lc["size_x"] / 2, -lc["size_y"] / 2, lc["size_y"] / 2,
lc["z_bottom"], lc["z_top"])
cut = rg.Brep.CreateBooleanDifference([lean], piles, TOL)
if not cut or len(cut) != 1:
raise RuntimeError("lean concrete boolean failed")
add(cut[0], "lean", f"{PID}-LC", "lean concrete", FCK["lean"], "D1 정면도, D1 단면도")
add(box(-ft["size_x"] / 2, ft["size_x"] / 2, -ft["size_y"] / 2, ft["size_y"] / 2,
ft["z_bottom"], ft_top), "footing", f"{PID}-FT", "footing", FCK["footing"],
"D1 정면도, D1 단면도, D2 기초평면도")
add(cylinder(0, 0, ft_top, col_top, cl["diameter"] / 2), "column", f"{PID}-CL", "column", FCK["column"],
"D1 정면도, D1 단면도, D1 단면 C-C/D-D/E-E, D2 기초평면도")
cop, centers = coping_brep(cp)
add(cop, "coping", f"{PID}-CP", "coping", FCK["coping"], "D1 평면도, D1 정면도, D1 단면도, D1 단면 A-A/B-B")
pile_role = f"{pl['kind']} D{pl['diameter']:.0f} L{pl['length'] / 1000:g}m"
for n, b in enumerate(piles, 1):
add(b, "pile", f"{PID}-PL{n:02d}", pile_role, "-", "D1 정면도, D1 단면도")
seats = add_bearing_seats(P["bearing_seats"], cp["top_steps"]) if "bearing_seats" in P else 0
doc.Views.Redraw()
print(json.dumps({"objects": 4 + len(piles) + seats, "coping_arc_centers": centers}))
main()
@@ -0,0 +1,205 @@
"""G1 stage, form "single shaft" (Ramp-A P1, 2026-10-13): drilled shaft in two diameters (no footing, no lean
concrete), circular column, Y-shaped coping (R soffit arcs flaring from the column, battered ends that are convex
in plan, a step in the top surface), one pedestal (box) and one h2 pad (frustum) per bearing.
Run inside Rhino 8 via runner with globals PARAMS_PATH. Units mm, Z = EL x 1000, origin = pier centre.
params:
pile {segments [{d, z0, z1}, ...] bottom to top}; column {shape "circular", diameter, z0, z1}
coping {form "y_shaft", z_bottom (= column top), z_tip, half_bottom (column face), soffit {centre_x, centre_z,
radius} (left arc; the right one is mirrored), z_top_left, z_top_right, top_half (straight top edge),
plan_end {corner_x, tip_x, half_width} (bottom edge arc through (corner_x, +-half_width) and (tip_x, 0)),
side {half_width_top, half_width_bottom} (2,200 above z_tip, straight taper to 2,000 at z_bottom)}
bearing_seats {pedestal {size_x, size_y}, pad_margin_bottom, pad_margin_top,
bearings [{no, centre [x, y], rotation_deg, seat_z, h1, h2, A, B, type}]}
Coping = (front outline extruded along Y) n (side outline extruded along X) - two end cutters. The end face is
ruled: straight top edge x = +-top_half at the top level, convex plan arc at z_tip. Each extrusion reaches past
the faces the other one defines (no coincident or tangent faces in the booleans, see concrete_tapered.py).
Not modelled (user decision 2026-10-13): the 30 mm decorative grooves (D1 detail "B").
"""
import json
import math
import Rhino.Geometry as rg
import System
from Rhino.DocObjects import ObjectAttributes
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
FCK = P["materials"]["fck"]
LAYERS = {k: (f"{PID}::{name}", Color.FromArgb(*rgb)) for k, (name, rgb) in P["display"]["concrete_layers"].items()}
def layer_index(full_path, color):
idx = doc.Layers.FindByFullPath(full_path, -1)
if idx >= 0:
return idx
parent_name, child = full_path.split("::")
parent = doc.Layers.FindByFullPath(parent_name, -1)
if parent < 0:
parent = doc.Layers.Add(parent_name, Color.Black)
layer = __import__("Rhino").DocObjects.Layer()
layer.Name, layer.Color = child, color
layer.ParentLayerId = doc.Layers[parent].Id
return doc.Layers.Add(layer)
def add(brep, key, oid, role, fck, views):
if brep is None or not brep.IsSolid:
raise RuntimeError(f"{oid}: not a closed solid")
if not brep.IsValid: # AddBrep silently refuses invalid breps
ok, log = brep.IsValidWithLog()
raise RuntimeError(f"{oid}: invalid brep: {log[:400]}")
attr = ObjectAttributes()
attr.LayerIndex = layer_index(*LAYERS[key])
attr.Name = oid
for k, v in (("id", oid), ("role", role), ("fck", fck), ("source_views", views)):
attr.SetUserString(k, v)
gid = doc.Objects.AddBrep(brep, attr)
if gid == System.Guid.Empty:
raise RuntimeError(f"{oid}: AddBrep failed")
return gid
def solid(b):
if b is not None and b.SolidOrientation == rg.BrepSolidOrientation.Inward:
b.Flip()
return b
def cylinder(z0, z1, r):
return rg.Cylinder(rg.Circle(rg.Plane(rg.Point3d(0, 0, z0), rg.Vector3d.ZAxis), r), z1 - z0).ToBrep(True, True)
def extrude_closed(curve, vec):
return solid(rg.Surface.CreateExtrusion(curve, vec).ToBrep().CapPlanarHoles(TOL))
def rotated_rect(cx, cy, along, across, rot_deg, z):
"""Rectangle with `along` side on +Y rotated clockwise by rot_deg (plan view)."""
t = math.radians(rot_deg)
ua, uc = (math.sin(t), math.cos(t)), (math.cos(t), -math.sin(t))
pts = [(cx + sa * ua[0] * along / 2 + sc * uc[0] * across / 2, cy + sa * ua[1] * along / 2 + sc * uc[1] * across / 2)
for sa, sc in ((-1, -1), (1, -1), (1, 1), (-1, 1))]
return rg.PolylineCurve([rg.Point3d(x, y, z) for x, y in pts + pts[:1]])
def frustum(bottom, top):
lofts = rg.Brep.CreateFromLoft([bottom, top], rg.Point3d.Unset, rg.Point3d.Unset, rg.LoftType.Straight, False)
return solid(lofts[0].CapPlanarHoles(TOL))
def boolean(kind, a, b):
f = rg.Brep.CreateBooleanIntersection if kind == "and" else rg.Brep.CreateBooleanDifference
out = f(a, b, TOL)
if not out or len(out) != 1:
raise RuntimeError(f"coping boolean {kind} failed ({len(out) if out else 0} pieces)")
return solid(out[0])
def front_extrusion(c):
"""Front outline (XZ): soffit arcs from the column face, continued past the end (cut later), vertical outer
ends at +-ext, top levels with the step at x = 0. Extruded over y = +-(half_width_top + 100)."""
s, zb = c["soffit"], c["z_bottom"]
cx, cz, r = s["centre_x"], s["centre_z"], s["radius"]
a0 = math.atan2(zb - cz, -c["half_bottom"] - cx) # left arc start (column face)
ext = c["plan_end"]["tip_x"] + 400 # past the tip (abs)
a1 = math.acos((-ext - cx) / r) if abs((-ext - cx) / r) <= 1 else None
if a1 is None:
raise RuntimeError("soffit arc does not reach the extension")
am = (a0 + a1) / 2 # left arc through start, mid, end
pl = [(cx + r * math.cos(a), cz + r * math.sin(a)) for a in (a0, am, a1)] # (x, z)
w = c["side"]["half_width_top"] + 100
to_xyz = lambda x, z: rg.Point3d(x, -w, z)
arc_l = rg.ArcCurve(rg.Arc(*[to_xyz(x, z) for x, z in pl]))
arc_r = rg.ArcCurve(rg.Arc(*[to_xyz(-x, z) for x, z in reversed(pl)]))
zl, zr = c["z_top_left"], c["z_top_right"]
xe, ze = pl[2] # left extension end (xe < 0)
poly_top = rg.PolylineCurve([to_xyz(xe, ze), to_xyz(xe, zl), to_xyz(0, zl), to_xyz(0, zr), to_xyz(-xe, zr),
to_xyz(-xe, ze)])
bottom = rg.LineCurve(to_xyz(c["half_bottom"], zb), to_xyz(-c["half_bottom"], zb))
j = rg.Curve.JoinCurves([arc_l, poly_top, arc_r, bottom], 0.01)
if len(j) != 1 or not j[0].IsClosed:
raise RuntimeError(f"front outline not closed ({len(j)} pieces)")
return extrude_closed(j[0], rg.Vector3d(0, 2 * w, 0))
def side_extrusion(c):
"""Side outline (YZ): straight taper from the column width at z_bottom to the full width at z_tip, continued
100 below z_bottom (no face on the coping bottom plane), vertical above, up past the top. Extruded over X."""
sd, zb, zt = c["side"], c["z_bottom"], c["z_tip"]
hb, ht = sd["half_width_bottom"], sd["half_width_top"]
k = (ht - hb) / (zt - zb)
y_low, z_low = hb - k * 100, zb - 100
ztop = max(c["z_top_left"], c["z_top_right"]) + 200
x0 = c["plan_end"]["tip_x"] + 600
pts = [(-y_low, z_low), (y_low, z_low), (ht, zt), (ht, ztop), (-ht, ztop), (-ht, zt)]
crv = rg.PolylineCurve([rg.Point3d(-x0, y, z) for y, z in pts + pts[:1]])
return extrude_closed(crv, rg.Vector3d(2 * x0, 0, 0))
def end_cutter(c, sx):
"""Waste beyond the ruled end face of one end (sx = -1 left, +1 right). Two straight-lofted sections, below the
tip level and above the top: on each, x(y) = arc(y) + t * (top_edge - arc(y)), t = (z - z_tip) / (z_top - z_tip),
so the loft lines are the rulings of the end face. Plan arc sampled every 25 mm (chord error < 0.1 mm)."""
pe, zt = c["plan_end"], c["z_tip"]
ztop = c["z_top_left"] if sx < 0 else c["z_top_right"]
hw, corner, tip, top = pe["half_width"], pe["corner_x"], pe["tip_x"], c["top_half"]
sag = tip - corner
R = (hw * hw + sag * sag) / (2 * sag)
ys = [-hw - 200 + 25 * i for i in range(int((2 * hw + 400) / 25) + 1)]
arc = lambda y: tip - (R - math.sqrt(R * R - y * y)) # abs x of the bottom edge
secs = []
for z in (zt - 700, ztop + 150):
t = (z - zt) / (ztop - zt)
pts = [(arc(y) + t * (top - arc(y)), y) for y in ys]
pts += [(tip + 1500, ys[-1]), (tip + 1500, ys[0])]
secs.append(rg.PolylineCurve([rg.Point3d(sx * x, y, z) for x, y in pts + pts[:1]]))
lofts = rg.Brep.CreateFromLoft(secs, rg.Point3d.Unset, rg.Point3d.Unset, rg.LoftType.Straight, False)
b = solid(lofts[0].CapPlanarHoles(TOL))
if b is None or not b.IsSolid:
raise RuntimeError("end cutter is not a closed solid")
return b
def coping_brep(c):
body = boolean("and", [front_extrusion(c)], [side_extrusion(c)])
return boolean("diff", [body], [end_cutter(c, -1), end_cutter(c, 1)])
def main():
for o in list(doc.Objects):
if o.Attributes.GetUserString("id"):
doc.Objects.Delete(o, True)
pl, cl, cp, bs = (P[k] for k in ("pile", "column", "coping", "bearing_seats"))
segs = pl["segments"]
for a, b in zip(segs, segs[1:]):
if abs(a["z1"] - b["z0"]) > TOL:
raise RuntimeError("pile segments not continuous")
if abs(segs[-1]["z1"] - cl["z0"]) > TOL or abs(cl["z1"] - cp["z_bottom"]) > TOL:
raise RuntimeError("pile top / column / coping bottom levels do not meet")
for i, s in enumerate(segs, 1):
add(cylinder(s["z0"], s["z1"], s["d"] / 2), "pile", f"{PID}-PL{i:02d}",
f"drilled shaft segment {i} D{s['d']:.0f}", FCK["pile"], "일반도 정면도·측면도")
add(cylinder(cl["z0"], cl["z1"], cl["diameter"] / 2), "column", f"{PID}-CL", "column", FCK["column"],
"일반도 정면도·측면도·기둥 단면도")
add(coping_brep(cp), "coping", f"{PID}-CP", "coping (Y-shaped, single shaft)", FCK["coping"],
"일반도 정면도·측면도·평면도")
ped, mb, mt = bs["pedestal"], bs["pad_margin_bottom"], bs["pad_margin_top"]
for b in bs["bearings"]:
cx, cy = b["centre"]
z0, z1 = b["seat_z"], b["seat_z"] + b["h1"]
base = rotated_rect(cx, cy, ped["size_y"], ped["size_x"], b["rotation_deg"], z0)
add(extrude_closed(base, rg.Vector3d(0, 0, b["h1"])), "seat", f"{PID}-BS{b['no']:02d}",
f"bearing pedestal h1 NO.{b['no']}", FCK["seat"], "일반도 평면도·교량받침 상세도·ELEVATION TABLE")
bottom = rotated_rect(cx, cy, b["A"] + mb, b["B"] + mb, b["rotation_deg"], z1)
top = rotated_rect(cx, cy, b["A"] + mt, b["B"] + mt, b["rotation_deg"], z1 + b["h2"])
add(frustum(bottom, top), "pad", f"{PID}-BP{b['no']:02d}", f"bearing pad h2 NO.{b['no']} {b['type']}", "-",
"일반도 평면도·교량받침 상세도·ELEVATION TABLE")
doc.Views.Redraw()
print(json.dumps({"objects": len(segs) + 2 + 2 * len(bs["bearings"])}))
main()
@@ -0,0 +1,267 @@
"""G1 stage, form "tapered T" (Ulsan P1, 2026-10-13): lean, spread footing (no piles), circular column, coping
with a straight sloped soffit, battered ends, a step in the top surface and rounded plan corners,
one pedestal (box) and one h2 pad (frustum) per bearing.
Run inside Rhino 8 via runner with globals PARAMS_PATH. Units mm, Z = EL x 1000, origin = pier centre.
params:
lean_concrete {size_x, size_y, z_bottom, z_top}; footing {size_x, size_y, z_bottom, thickness}
column {shape "circular", diameter, height}
coping {form "tapered", profile_xz [[x, z], ...] (front-view outline, closed implicitly), width_y,
plan_length (at the tips), top_length, z_tip, z_top_left, z_top_right, plan_corner_radius}
bearing_seats {pedestal {size_x, size_y}, pad_margin_bottom, pad_margin_top,
bearings [{no, centre [x, y], rotation_deg, seat_z, h1, h2, A, B, type}]}
Coping = profile extrusion minus the four plan-corner wastes outside R (faceted 16-segment arcs, +0.5 mm).
Optional (Okseong P1, 2026-10-14; absent -> the code paths above are unchanged):
piles {count_x, count_y, pitch_x, pitch_y, diameter, length, head_embedment, kind}: pile grid under the footing,
the lean concrete is cut by the piles (as concrete.py)
coping.side_taper {half_width_top, z_taper_top, half_width_bottom}: body intersected with the side outline
(full width above z_taper_top, straight taper to the bottom width at the coping bottom)
coping.skew_deg: plan skew; the coping is sheared x' = x - tan(skew) * y (end faces / top-step lines at 90 + skew)
footing.rot_deg (Ulsan P2, 2026-10-15; absent = 0): lean, footing and piles are drawn in the foundation's own axes and then
rotated counter-clockwise (plan view, X right / Y up) about the pier centre; column, coping and seats stay in the pier axes
bearing_seats.pedestals [{no, corners [[x, y] x4], z_bottom, top {y0, z0, y1, z1}}]: explicit pedestal solids (plan
quadrilateral, top plane linear in y); bearings[].pedestal = its index; pads then sit on that plane
"""
import json
import math
import Rhino.Geometry as rg
import System
from Rhino.DocObjects import ObjectAttributes
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
FCK = P["materials"]["fck"]
LAYERS = {k: (f"{PID}::{name}", Color.FromArgb(*rgb)) for k, (name, rgb) in P["display"]["concrete_layers"].items()}
def layer_index(full_path, color):
idx = doc.Layers.FindByFullPath(full_path, -1)
if idx >= 0:
return idx
parent_name, child = full_path.split("::")
parent = doc.Layers.FindByFullPath(parent_name, -1)
if parent < 0:
parent = doc.Layers.Add(parent_name, Color.Black)
layer = __import__("Rhino").DocObjects.Layer()
layer.Name, layer.Color = child, color
layer.ParentLayerId = doc.Layers[parent].Id
return doc.Layers.Add(layer)
def add(brep, key, oid, role, fck, views):
if brep is None or not brep.IsSolid:
raise RuntimeError(f"{oid}: not a closed solid")
attr = ObjectAttributes()
attr.LayerIndex = layer_index(*LAYERS[key])
attr.Name = oid
for k, v in (("id", oid), ("role", role), ("fck", fck), ("source_views", views)):
attr.SetUserString(k, v)
if not brep.IsValid: # AddBrep silently refuses invalid breps (2026-10-13: coping lost)
ok, log = brep.IsValidWithLog()
raise RuntimeError(f"{oid}: invalid brep: {log[:400]}")
gid = doc.Objects.AddBrep(brep, attr)
if gid == System.Guid.Empty:
raise RuntimeError(f"{oid}: AddBrep failed")
return gid
def solid(b):
if b is not None and b.SolidOrientation == rg.BrepSolidOrientation.Inward:
b.Flip()
return b
def box(x0, x1, y0, y1, z0, z1):
return rg.Box(rg.BoundingBox(x0, y0, z0, x1, y1, z1)).ToBrep()
def cylinder(x, y, z0, z1, r):
return rg.Cylinder(rg.Circle(rg.Plane(rg.Point3d(x, y, z0), rg.Vector3d.ZAxis), r), z1 - z0).ToBrep(True, True)
def extrude_closed(curve, vec):
return solid(rg.Surface.CreateExtrusion(curve, vec).ToBrep().CapPlanarHoles(TOL))
def rotated_rect(cx, cy, along, across, rot_deg, z):
"""Rectangle with `along` side on +Y rotated clockwise by rot_deg (plan view)."""
t = math.radians(rot_deg)
ua, uc = (math.sin(t), math.cos(t)), (math.cos(t), -math.sin(t))
pts = [(cx + sa * ua[0] * along / 2 + sc * uc[0] * across / 2, cy + sa * ua[1] * along / 2 + sc * uc[1] * across / 2)
for sa, sc in ((-1, -1), (1, -1), (1, 1), (-1, 1))]
return rg.PolylineCurve([rg.Point3d(x, y, z) for x, y in pts + pts[:1]])
def frustum(bottom, top):
lofts = rg.Brep.CreateFromLoft([bottom, top], rg.Point3d.Unset, rg.Point3d.Unset, rg.LoftType.Straight, False)
return solid(lofts[0].CapPlanarHoles(TOL))
def rrect(length, width, r, z):
"""Closed rounded rectangle centred on the axis at height z (corners filleted with radius r)."""
rect = rg.Rectangle3d(rg.Plane(rg.Point3d(0, 0, z), rg.Vector3d.ZAxis), rg.Interval(-length / 2, length / 2),
rg.Interval(-width / 2, width / 2)).ToNurbsCurve()
return rg.Curve.CreateFilletCornersCurve(rect, r, TOL, math.radians(1)) if r > 0 else rect
def corner_waste(sx, sy, c, z_top):
"""Region outside the R fillet of one plan corner (x sign sx, y sign sy), as a straight loft through three
equal sections: vertical below the tip level (fillet centre at tip - R), then following the end batter up to
z_top + 100 (centre moves with the end face). The arc is a 16-segment polyline offset 0.5 mm outwards
(chord error < 0.6 mm), so no cutter face is tangent to or coincident with a coping face / knife edge."""
w, R = c["width_y"], c["plan_corner_radius"]
tip, top, z_tip = c["plan_length"] / 2, c["top_length"] / 2, c["z_tip"]
batter = (tip - top) / (z_top - z_tip) # x shift per mm of height above the tip
zs = [z_tip - 300, z_tip, z_top + 100]
cxs = [tip - R, tip - R, tip - R - batter * (z_top + 100 - z_tip)]
cy, r = w / 2 - R, R + 0.5
secs = []
for z, cx in zip(zs, cxs):
pts = [(cx + r * math.cos(math.radians(a)), cy + r * math.sin(math.radians(a))) for a in
[90 * i / 16 for i in range(17)]]
pts += [(cx, cy + R + 100), (cx + R + 100, cy + R + 100), (cx + R + 100, cy)]
secs.append(rg.PolylineCurve([rg.Point3d(sx * x, sy * y, z) for x, y in pts + pts[:1]]))
lofts = rg.Brep.CreateFromLoft(secs, rg.Point3d.Unset, rg.Point3d.Unset, rg.LoftType.Straight, False)
return solid(lofts[0].CapPlanarHoles(TOL))
def side_cut(c, body):
"""Intersect with the side outline extruded along X (Ramp-A style taper); reaches past the X ends."""
st = c["side_taper"]
zb = min(z for _, z in c["profile_xz"])
zt, ht, hb = st["z_taper_top"], st["half_width_top"], st["half_width_bottom"]
k = (ht - hb) / (zt - zb)
y_low, z_low = hb - k * 100, zb - 100
ztop = max(c["z_top_left"], c["z_top_right"], max(z for _, z in c["profile_xz"])) + 200
x0 = c["plan_length"] / 2 + 3000
pts = [(-y_low, z_low), (y_low, z_low), (ht, zt), (ht, ztop), (-ht, ztop), (-ht, zt)]
side = extrude_closed(rg.PolylineCurve([rg.Point3d(-x0, y, z) for y, z in pts + pts[:1]]), rg.Vector3d(2 * x0, 0, 0))
out = rg.Brep.CreateBooleanIntersection(body, side, TOL)
if not out or len(out) != 1:
raise RuntimeError(f"coping side taper failed ({len(out) if out else 0})")
return solid(out[0])
def skew_xf(deg):
xf = rg.Transform.Identity
xf.M01 = -math.tan(math.radians(deg)) # x' = x - tan(skew) * y
return xf
def coping_brep(c):
"""Front profile (sloped soffit, battered ends, top step) extruded over the width as one solid, minus the four
plan-corner wastes outside the R fillets (see corner_waste). Splitting the coping into parts and uniting them,
or filleting the corner edges, failed on coplanar faces / the knife-edge tips (2026-10-13)."""
w, R = c["width_y"], c["plan_corner_radius"]
if "side_taper" in c or "skew_deg" in c: # Okseong form (2026-10-14): extrusion wider than the taper
w2 = w + 400
prof = rg.PolylineCurve([rg.Point3d(x, -w2 / 2, z) for x, z in c["profile_xz"] + c["profile_xz"][:1]])
a = extrude_closed(prof, rg.Vector3d(0, w2, 0))
if "side_taper" in c:
a = side_cut(c, a)
if c.get("skew_deg"):
a.Transform(skew_xf(c["skew_deg"]))
return a
prof = rg.PolylineCurve([rg.Point3d(x, -w / 2, z) for x, z in c["profile_xz"] + c["profile_xz"][:1]])
a = extrude_closed(prof, rg.Vector3d(0, w, 0))
if R <= 0:
return a
cutters = [corner_waste(sx, sy, c, c["z_top_left"] if sx < 0 else c["z_top_right"]) for sx in (1, -1) for sy in (1, -1)]
for q in cutters:
if q is None or not q.IsSolid:
raise RuntimeError("corner cutter is not a closed solid")
out = rg.Brep.CreateBooleanDifference([a], cutters, TOL)
if not out or len(out) != 1:
raise RuntimeError(f"coping corner cut failed ({len(out) if out else 0})")
return solid(out[0])
def plane_z(top, y):
return top["z0"] + (y - top["y0"]) * (top["z1"] - top["z0"]) / (top["y1"] - top["y0"])
def add_shared_seats(bs):
"""Pedestal = plan quadrilateral extruded from z_bottom to its sloped top plane; pad = frustum whose bottom lies
on that plane and whose top is flat at (pedestal top at the bearing centre) + h2 (Okseong P1, O2 2026-10-14)."""
mb, mt = bs["pad_margin_bottom"], bs["pad_margin_top"]
for pd in bs["pedestals"]:
cs = pd["corners"]
bot = [rg.Point3d(x, y, pd["z_bottom"]) for x, y in cs]
top = [rg.Point3d(x, y, plane_z(pd["top"], y)) for x, y in cs]
b = rg.Brep.CreateFromBox(bot + top)
if b is None or not b.IsSolid:
raise RuntimeError(f"pedestal {pd['no']} failed")
add(solid(b), "seat", f"{PID}-BS{pd['no']:02d}", f"bearing pedestal (shared) NO.{pd['no']}", FCK["seat"],
"일반도 평면도·교량받침 상세·ELEVATION TABLE")
for b in bs["bearings"]:
top = bs["pedestals"][b["pedestal"]]["top"]
cx, cy = b["centre"]
bottom = rotated_rect(cx, cy, b["A"] + mb, b["B"] + mb, b["rotation_deg"], 0)
bottom = rg.PolylineCurve([rg.Point3d(p.X, p.Y, plane_z(top, p.Y)) for p in bottom.ToPolyline()])
zt = plane_z(top, cy) + b["h2"]
add(frustum(bottom, rotated_rect(cx, cy, b["A"] + mt, b["B"] + mt, b["rotation_deg"], zt)), "pad",
f"{PID}-BP{b['no']:02d}", f"bearing pad h2 NO.{b['no']} {b['type']}", "-", "일반도 교량받침 상세·ELEVATION TABLE")
def main():
for o in list(doc.Objects):
if o.Attributes.GetUserString("id"):
doc.Objects.Delete(o, True)
lc, ft, cl, cp, bs = (P[k] for k in ("lean_concrete", "footing", "column", "coping", "bearing_seats"))
ft_top = ft["z_bottom"] + ft["thickness"]
col_top = ft_top + cl["height"]
zmin = min(z for _, z in cp["profile_xz"])
if abs(col_top - zmin) > TOL:
raise RuntimeError(f"column top {col_top} != coping bottom {zmin}")
rot = rg.Transform.Rotation(math.radians(ft.get("rot_deg", 0.0)), rg.Vector3d.ZAxis, rg.Point3d.Origin)
lean = box(-lc["size_x"] / 2, lc["size_x"] / 2, -lc["size_y"] / 2, lc["size_y"] / 2, lc["z_bottom"], lc["z_top"])
piles = []
if "piles" in P: # Okseong P1 (2026-10-14): pile grid, lean cut by the piles
pl = P["piles"]
top = ft["z_bottom"] + pl["head_embedment"]
piles = [cylinder(-(pl["count_x"] - 1) * pl["pitch_x"] / 2 + i * pl["pitch_x"],
-(pl["count_y"] - 1) * pl["pitch_y"] / 2 + j * pl["pitch_y"], top - pl["length"], top,
pl["diameter"] / 2) for j in range(pl["count_y"]) for i in range(pl["count_x"])]
cut = rg.Brep.CreateBooleanDifference([lean], piles, TOL)
if not cut or len(cut) != 1:
raise RuntimeError("lean concrete boolean failed")
lean = solid(cut[0])
footing = box(-ft["size_x"] / 2, ft["size_x"] / 2, -ft["size_y"] / 2, ft["size_y"] / 2, ft["z_bottom"], ft_top)
if ft.get("rot_deg"):
for b in [lean, footing] + piles:
b.Transform(rot)
add(lean, "lean", f"{PID}-LC", "lean concrete", FCK["lean"], "일반도 정면도·단면도")
add(footing, "footing", f"{PID}-FT", "footing on piles" if piles else "spread footing (no piles)", FCK["footing"],
"일반도 정면도·단면도·기초 평면도")
add(cylinder(0, 0, ft_top, col_top, cl["diameter"] / 2), "column", f"{PID}-CL", "column", FCK["column"],
"일반도 정면도·단면도·단면 A-A")
add(coping_brep(cp), "coping", f"{PID}-CP", "coping (tapered T)", FCK["coping"], "일반도 평면도·정면도·단면도")
for n, b in enumerate(piles, 1):
pl = P["piles"]
add(b, "pile", f"{PID}-PL{n:02d}", f"{pl['kind']} D{pl['diameter']:.0f} L{pl['length'] / 1000:g}m", "-",
"일반도 정면도·단면도·기초 평면도")
if "pedestals" in bs: # Okseong P1: shared pedestals with a sloped top
add_shared_seats(bs)
doc.Views.Redraw()
print(json.dumps({"objects": 4 + len(piles) + len(bs["pedestals"]) + len(bs["bearings"])}))
return
ped, mb, mt = bs["pedestal"], bs["pad_margin_bottom"], bs["pad_margin_top"]
for b in bs["bearings"]:
cx, cy = b["centre"]
z0, z1 = b["seat_z"], b["seat_z"] + b["h1"]
base = rotated_rect(cx, cy, ped["size_y"], ped["size_x"], b["rotation_deg"], z0)
add(extrude_closed(base, rg.Vector3d(0, 0, b["h1"])), "seat", f"{PID}-BS{b['no']:02d}",
f"bearing pedestal h1 NO.{b['no']}", FCK["seat"], "일반도 교량받침 상세·ELEVATION TABLE")
bottom = rotated_rect(cx, cy, b["A"] + mb, b["B"] + mb, b["rotation_deg"], z1)
top = rotated_rect(cx, cy, b["A"] + mt, b["B"] + mt, b["rotation_deg"], z1 + b["h2"])
add(frustum(bottom, top), "pad", f"{PID}-BP{b['no']:02d}", f"bearing pad h2 NO.{b['no']} {b['type']}", "-",
"일반도 교량받침 상세·ELEVATION TABLE")
doc.Views.Redraw()
print(json.dumps({"objects": 4 + 2 * len(bs["bearings"])}))
main()
@@ -0,0 +1,241 @@
"""G4 stage: coping rebar C1-C16, S1, S2 (centreline + pipe) from params `coping_rebar`.
Run inside Rhino 8 via runner (globals PARAMS_PATH) after concrete.py, then rebar_colours.py.
Ported from Code/교각모델링/build_coping_rebar_rhino.py (Ungok P1, model v0.1.09). Rules: 설계서 v0.1.07 §7. Units mm, X transverse, Y longitudinal, Z = EL x 1000.
"""
import json
import math
import Rhino
import Rhino.Geometry as rg
from Rhino.DocObjects import ObjectAttributes
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items() if m[0] in "CS"}
DIA = {d: float(d[1:]) for d, _ in MARK.values()} # 'H29' -> 29.0
CL_RGB = tuple(P["display"]["centreline_colour"])
CP_RGB = tuple(P["display"]["coping_layer_colour"])
ARC_C = tuple(P["coping_rebar"]["soffit_arc_centre"]) # soffit arc centre (right); left is mirrored
# coping end faces (concrete) from the coping outline: bottom tip (+-half, zb+rise) -> top (+-(half-inset), end step z)
_C = P["coping"]
HALF, INSET = _C["length_bottom_tip_to_tip"] / 2, _C["end_batter_top_inset"]
Z_TIP = _C["z_bottom_flat"] + _C["tip_bottom_rise"]
ZL, ZR = _C["top_steps"][0]["z"], _C["top_steps"][-1]["z"]
def xl(z):
return -(HALF - INSET) - INSET * (ZL - z) / (ZL - Z_TIP)
def xr(z):
return (HALF - INSET) + INSET * (ZR - z) / (ZR - Z_TIP)
def unit(dx, dz):
n = math.hypot(dx, dz)
return dx / n, dz / n
U_LEFT_DOWN, U_RIGHT_DOWN = unit(-INSET, -(ZL - Z_TIP)), unit(INSET, -(ZR - Z_TIP))
def layer(path, color):
parent = -1
parts = path.split("::")
for i in range(1, len(parts) + 1):
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
if idx < 0:
lay = Rhino.DocObjects.Layer()
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
if parent >= 0:
lay.ParentLayerId = doc.Layers[parent].Id
idx = doc.Layers.Add(lay)
parent = idx
return parent
def min_r(dia):
d = DIA[dia]
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2 # box rule (REBAR_BENDS_20260914)
def fillet(curve, dia):
f = rg.Curve.CreateFilletCornersCurve(curve, min_r(dia), TOL, math.radians(1))
return f or curve
def add_bar(curve, mark, occ, design_len, src, extra=None):
dia, shape = MARK[mark]
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
if not pipes or not pipes[0].IsSolid:
raise RuntimeError(f"{mark} {occ}: pipe failed")
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-CP", anchor_hosts="",
design_length=f"{design_len:.3f}", coupler_length="0.000", src=src)
common.update(extra or {})
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
(pipes[0], layer(f"{PID}::철근::코핑::{mark}", CP_RGB), f"{PID}-R")):
attr = ObjectAttributes()
attr.LayerIndex = lay
oid = f"{prefix}-{mark}-{occ}"
attr.Name = oid
attr.SetUserString("id", oid)
for k, v in common.items():
attr.SetUserString(k, v)
(doc.Objects.AddBrep if isinstance(geo, rg.Brep) else doc.Objects.AddCurve)(geo, attr)
def soffit_offset_z(x, cp):
st = cp["stirrups"]
if abs(x) <= cp["bottom"]["half_flat"]:
return cp["bottom"]["z_flat"]
cx = ARC_C[0] if x > 0 else -ARC_C[0]
R = cp["bottom"]["R_offset"]
return ARC_C[1] + math.sqrt(R * R - (x - cx) ** 2)
def stirrup_top(x, cp):
st = cp["stirrups"]
bands = st["top_right"] if x >= 0 else st["top_left"]
return [z for x0, z in bands if abs(x) >= x0][-1]
def main():
for o in list(doc.Objects):
oid = o.Attributes.GetUserString("id") or ""
if oid.startswith(tuple(f"{PID}-{k}-{m}" for k in ("R", "RC") for m in "CS")):
doc.Objects.Delete(o, True)
cp = P["coping_rebar"]
ys = cp["y_positions"]
n = 0
# top bars (VIII)
for tier, t in cp["top"]["tiers"].items():
z, a, (bl, br) = t["z"], t["a"], t["legs"]
d = (xr(z) - xl(z) - a) / 2
x0, x1 = xl(z) + d, xr(z) - d
for i, y in enumerate(ys):
mark = t["marks"][i % 2]
pts = [rg.Point3d(x0 + U_LEFT_DOWN[0] * bl, y, z + U_LEFT_DOWN[1] * bl), rg.Point3d(x0, y, z),
rg.Point3d(x1, y, z), rg.Point3d(x1 + U_RIGHT_DOWN[0] * br, y, z + U_RIGHT_DOWN[1] * br)]
add_bar(fillet(rg.PolylineCurve(pts), MARK[mark][0]), mark, f"t{tier}y{y:+.0f}", a + bl + br,
f"tier {tier} EL {z / 1000:.3f}; end inset {d:.1f}; legs parallel to end faces",
{"a_model": f"{x1 - x0:.3f}"})
n += 1
# bottom bars (VII)
b = cp["bottom"]
R, zf = b["R_offset"], b["z_flat"]
hf = ARC_C[0] - math.sqrt(R * R - (zf - ARC_C[1]) ** 2) # exact flat/arc junction (params 1,073.9 is rounded)
dth = b["arc_len"] / R
for i, y in enumerate(ys):
mark = b["marks"][i % 2]
pieces = [rg.LineCurve(rg.Point3d(-hf, y, zf), rg.Point3d(hf, y, zf))]
for side in (1, -1):
cx = side * ARC_C[0]
th0 = math.atan2(zf - ARC_C[1], side * hf - cx)
th1 = th0 - side * dth
thm = (th0 + th1) / 2
p = lambda th: rg.Point3d(cx + R * math.cos(th), y, ARC_C[1] + R * math.sin(th))
arc = rg.ArcCurve(rg.Arc(p(th0), p(thm), p(th1)))
up = (-U_RIGHT_DOWN[0], -U_RIGHT_DOWN[1]) if side > 0 else (-U_LEFT_DOWN[0], -U_LEFT_DOWN[1])
e = p(th1)
leg = rg.LineCurve(e, rg.Point3d(e.X + up[0] * b["leg"], y, e.Z + up[1] * b["leg"]))
pieces += [arc, leg]
joined = rg.Curve.JoinCurves(pieces, TOL)
if len(joined) != 1:
raise RuntimeError(f"{mark} y{y}: bottom bar did not join ({len(joined)} pieces)")
c = joined[0]
add_bar(c, mark, f"y{y:+.0f}", 2 * hf + 2 * b["arc_len"] + 2 * b["leg"],
"flat EL 40.511 +-1,073.9; arcs R 12,100 concentric with soffit, length 5,273; legs 650 // end faces")
n += 1
# stirrups (IV as closed rectangles, widths as drawn in section A-A)
st = cp["stirrups"]
for mark, xs in (("C10", st["x_c10"]), ("C11", st["x_c11"]), ("C9", st["x_c9"])):
for x in xs:
zb, zt = soffit_offset_z(x, cp), stirrup_top(x, cp)
for k, (y0, y1) in enumerate(st["loops_y"]):
ym = (y0 + y1) / 2
pts = [rg.Point3d(x, ym, zb), rg.Point3d(x, y1, zb), rg.Point3d(x, y1, zt), rg.Point3d(x, y0, zt),
rg.Point3d(x, y0, zb), rg.Point3d(x, ym, zb)]
c = fillet(rg.PolylineCurve(pts), MARK[mark][0])
w, h = y1 - y0, zt - zb
b_s = h + st["b_offset"]
add_bar(c, mark, f"x{x:+.0f}k{k + 1}", 2 * w + 2 * b_s + 2 * st["c_sched"],
f"X {x}; loop {k + 1} Y {y0}..{y1}; laps 2x750 not drawn (length only)",
{"b_sched": f"{b_s:.1f}", "w_model": f"{w:.1f}"})
n += 1
# horizontal closed loops C12 (lower 6 levels) / C13 (upper 3 levels), one inside each stirrup
lp = cp.get("loops")
if lp:
R, inset = cp["bottom"]["R_offset"], lp["end_inset_face"]
for z, mark in zip(lp["levels"], lp["marks_by_level"]):
disc = R * R - (z - ARC_C[1]) ** 2
xs = ARC_C[0] - math.sqrt(disc) if disc > 0 else 1e9 # soffit-offset limit (symmetric)
x0, x1 = max(-xs, xl(z) + inset), min(xs, xr(z) - inset)
for k, (y0, y1) in enumerate(lp["loops_y"]):
xm = (x0 + x1) / 2
pts = [rg.Point3d(xm, y0, z), rg.Point3d(x1, y0, z), rg.Point3d(x1, y1, z), rg.Point3d(x0, y1, z),
rg.Point3d(x0, y0, z), rg.Point3d(xm, y0, z)]
c = fillet(rg.PolylineCurve(pts), MARK[mark][0])
a_m, w = x1 - x0, y1 - y0
add_bar(c, mark, f"z{z}k{k + 1}", 2 * (a_m + lp["a_offset"]) + 2 * w + 4 * lp["c_sched"],
f"EL {z / 1000:.3f}; X {x0:.1f}..{x1:.1f}; loop {k + 1} Y {y0}..{y1}; laps 4x580 length only",
{"a_sched": f"{a_m + lp['a_offset']:.1f}", "w_model": f"{w:.1f}"})
n += 1
# C14 transverse (Y) bars with hooks down; C15 / C16 longitudinal (X) bars in the raised step zones
if "c14" in cp:
c14 = cp["c14"]
h, hk = c14["a"] / 2, c14["hook"]
up = [tuple(v) for v in c14.get("hooks_up", [])]
for x, z in c14["points"]:
s = 1 if (x, z) in up else -1 # hooks up only where a down hook would leave the soffit
pts = [rg.Point3d(x, -h, z + s * hk), rg.Point3d(x, -h, z), rg.Point3d(x, h, z), rg.Point3d(x, h, z + s * hk)]
add_bar(fillet(rg.PolylineCurve(pts), MARK["C14"][0]), "C14", f"x{x:+.0f}z{z:.0f}", c14["a"] + 2 * hk,
"D1 정면도 점; hooks " + ("up (HITL fix)" if s > 0 else "down [추론]"))
n += 1
c15, c16 = cp["c15"], cp["c16"]
for y in c15["y_positions"]:
z, (h0, h1) = c15["z"], c15["hooks"]
pts = [rg.Point3d(c15["x0"], y, z - h0), rg.Point3d(c15["x0"], y, z), rg.Point3d(c15["x1"], y, z),
rg.Point3d(c15["x1"], y, z - h1)]
add_bar(fillet(rg.PolylineCurve(pts), MARK["C15"][0]), "C15", f"y{y:+.0f}", c15["x1"] - c15["x0"] + h0 + h1,
"D1 정면도 43,598.4 / 3,816~5,900")
z = c16["z"]
pts = [rg.Point3d(c16["x0"], y, z - c16["hook_left"]), rg.Point3d(c16["x0"], y, z),
rg.Point3d(c16["x1"], y, z)]
add_bar(fillet(rg.PolylineCurve(pts), MARK["C16"][0]), "C16", f"y{y:+.0f}", c16["x1"] - c16["x0"] + c16["hook_left"],
"D1 정면도 43,454.7 / -1,195~4,066; hook at left end [추론]")
n += 2
# S1 / S2 bearing-seat reinforcement under each pedestal (rotated with the pedestal)
if "s1s2" in cp and "bearing_seats" in P:
bs, ss = P["bearing_seats"], cp["s1s2"]
corners = bs["pedestal_girder5"]["front_half"] + bs["pedestal_girder5"]["rear_half"]
c5 = (sum(p[0] for p in corners) / len(corners), sum(p[1] for p in corners) / len(corners))
t = math.radians(bs["pedestal_girder5"]["rotation_deg"])
ua, uc = (math.sin(t), math.cos(t)), (math.cos(t), -math.sin(t)) # along (2,500) / across (1,200)
steps = P["coping"]["top_steps"]
for g in range(1, len(steps) + 1):
cx_, cy_ = c5[0] + (g - bs["reference_girder"]) * bs["pitch_x"], c5[1]
top = steps[g - 1]["z"]
pt = lambda s_al, s_ac, z: rg.Point3d(cx_ + ua[0] * s_al + uc[0] * s_ac, cy_ + ua[1] * s_al + uc[1] * s_ac, z)
s1 = ss["s1"]
for off in s1["offsets_across"]:
z, h = top - s1["z_below_top"], s1["a"] / 2
pts = [pt(-h, off, z - s1["leg"]), pt(-h, off, z), pt(h, off, z), pt(h, off, z - s1["leg"])]
add_bar(fillet(rg.PolylineCurve(pts), MARK["S1"][0]), "S1", f"g{g}c{off:+.0f}", s1["a"] + 2 * s1["leg"],
f"girder {g} pedestal; upper layer [추론]")
n += 1
s2 = ss["s2"]
for off in s2["offsets_along"]:
z, h = top - s2["z_below_top"], s2["a"] / 2
pts = [pt(off, -h, z - s2["leg"]), pt(off, -h, z), pt(off, h, z), pt(off, h, z - s2["leg"])]
add_bar(fillet(rg.PolylineCurve(pts), MARK["S2"][0]), "S2", f"g{g}a{off:+.0f}", s2["a"] + 2 * s2["leg"],
f"girder {g} pedestal; lower layer")
n += 1
doc.Views.Redraw()
print(json.dumps({"coping_bars": n}))
main()
@@ -0,0 +1,23 @@
"""Final stage: apply the display state recorded in params `display` (hidden objects).
Run inside Rhino 8 via runner (globals PARAMS_PATH) after all build stages.
"""
import json
from Rhino.DocObjects import ObjectEnumeratorSettings
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
hide = set(P["display"].get("hidden_ids", []))
s = ObjectEnumeratorSettings()
s.HiddenObjects = True
done = []
for o in doc.Objects.GetObjectList(s):
oid = o.Attributes.GetUserString("id") or ""
if oid in hide and not o.IsHidden:
doc.Objects.Hide(o, True)
done.append(oid)
missing = sorted(hide - {o.Attributes.GetUserString("id") for o in doc.Objects.GetObjectList(s)})
print(json.dumps({"hidden": done, "missing": missing}))
if missing:
raise RuntimeError(f"display: hidden_ids not found {missing}")
@@ -0,0 +1,52 @@
"""Fingerprint a .3dm (run inside Rhino 8 via runner). Globals: MODEL_PATH, OUT_PATH, optional FAST.
Opens MODEL_PATH headless (the active doc is untouched) and writes, per object id (user string `id`,
falling back to the object name): layer path, geometry type, visibility, colour, bbox (0.01 mm),
brep face count / solidity / volume (mm3, 1 decimal), curve length (0.001 mm) and all user strings.
Hidden and locked objects are included.
"""
import json
import Rhino
import Rhino.Geometry as rg
from Rhino.DocObjects import ObjectEnumeratorSettings
FAST = globals().get("FAST", False)
d = Rhino.RhinoDoc.OpenHeadless(MODEL_PATH)
if d is None:
raise RuntimeError(f"cannot open {MODEL_PATH}")
try:
s = ObjectEnumeratorSettings()
s.HiddenObjects = True
s.LockedObjects = True
objs, layers = {}, {}
for o in d.Objects.GetObjectList(s):
a = o.Attributes
oid = a.GetUserString("id") or a.Name or str(o.Id)
lay = d.Layers[a.LayerIndex]
layers[lay.FullPath] = [lay.Color.R, lay.Color.G, lay.Color.B, lay.IsVisible]
g = o.Geometry
bb = g.GetBoundingBox(True)
rec = {"layer": lay.FullPath, "type": type(g).__name__, "hidden": o.IsHidden, "locked": o.IsLocked,
"colour_source": int(a.ColorSource), "colour": [a.ObjectColor.R, a.ObjectColor.G, a.ObjectColor.B],
"bbox": [round(v, 2) for v in (bb.Min.X, bb.Min.Y, bb.Min.Z, bb.Max.X, bb.Max.Y, bb.Max.Z)],
"user": {k: a.GetUserString(k) for k in sorted(a.GetUserStrings().AllKeys)}}
b = g if isinstance(g, rg.Brep) else (rg.Brep.TryConvertBrep(g) if isinstance(g, rg.Extrusion) else None)
if b is not None:
rec["faces"], rec["solid"] = b.Faces.Count, b.IsSolid
if FAST and "-R-" in oid: # rebar pipe: bbox + face count only (volume dominates run time)
rec["volume"] = "skipped"
else:
mp = rg.VolumeMassProperties.Compute(b) if b.IsSolid else None
rec["volume"] = round(mp.Volume, 1) if mp else None
elif isinstance(g, rg.Curve):
rec["length"] = round(g.GetLength(), 3)
if oid in objs:
oid = f"{oid}#dup{sum(k.startswith(oid) for k in objs)}"
objs[oid] = rec
out = {"model": MODEL_PATH, "units": str(d.ModelUnitSystem), "tolerance": d.ModelAbsoluteTolerance,
"count": len(objs), "layers": layers, "objects": objs}
json.dump(out, open(OUT_PATH, "w", encoding="utf-8"), ensure_ascii=False)
print(json.dumps({"count": len(objs), "units": out["units"], "tolerance": out["tolerance"]}))
finally:
d.Dispose()
@@ -0,0 +1,186 @@
"""G3 stage: footing rebar B1-B12 (centreline + pipe solid) from params `footing_rebar`.
Run inside Rhino 8 via runner (globals PARAMS_PATH) after concrete.py.
Ported from Code/교각모델링/build_footing_rebar_rhino.py (Ungok P1, model v0.1.09).
Rules: 설계서 v0.1.06 §6 (HITL F1-F3 approved 2026-10-07). z in params is above the footing bottom.
`design_length` = schedule original length (a, a+2b, 2(a+b), a+b+c); curve length is measured separately.
"""
import json
import math
import Rhino
import Rhino.Geometry as rg
from Rhino.DocObjects import ObjectAttributes
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
FB = P["footing"]["z_bottom"]
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items() if m.startswith("B")}
DIA = {d: float(d[1:]) for d, _ in MARK.values()} # 'S25' -> 25.0
CL_RGB = tuple(P["display"]["centreline_colour"])
FT_RGB = tuple(P["display"]["footing_layer_colour"])
LEGS = P["footing_rebar"]["legs"]
def leg(mark):
"""Signed end-leg length of a footing bar mark (+ up / - down) from params `legs`."""
v = LEGS[mark]
n = v.get("leg", v.get("hook"))
return n if v["dir"].startswith("up") else -n
def layer(path, color):
parent = -1
parts = path.split("::")
for i in range(1, len(parts) + 1):
sub = "::".join(parts[:i])
idx = doc.Layers.FindByFullPath(sub, -1)
if idx < 0:
lay = Rhino.DocObjects.Layer()
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
if parent >= 0:
lay.ParentLayerId = doc.Layers[parent].Id
idx = doc.Layers.Add(lay)
parent = idx
return parent
def min_r(dia):
d = DIA[dia]
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2 # box rule (REBAR_BENDS_20260914)
def poly(pts, dia):
pl = rg.PolylineCurve([rg.Point3d(*p) for p in pts])
if len(pts) < 3:
return pl
f = rg.Curve.CreateFilletCornersCurve(pl, min_r(dia), TOL, math.radians(1))
return f or pl
def add_bar(curve, mark, occ, design_len, src):
dia, shape = MARK[mark]
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
if not pipes or not pipes[0].IsSolid:
raise RuntimeError(f"{mark} {occ}: pipe failed")
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-FT", anchor_hosts="",
design_length=f"{design_len:.3f}", coupler_length="0.000", src=src)
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
(pipes[0], layer(f"{PID}::철근::기초::{mark}", FT_RGB), f"{PID}-R")):
attr = ObjectAttributes()
attr.LayerIndex = lay
oid = f"{prefix}-{mark}-{occ}"
attr.Name = oid
attr.SetUserString("id", oid)
for k, v in common.items():
attr.SetUserString(k, v)
(doc.Objects.AddBrep if isinstance(geo, rg.Brep) else doc.Objects.AddCurve)(geo, attr)
def bar_y(x, z, a, end, dz):
"""Bar along Y (length a) at x, height z; end legs of length dz (+up / -down) or none."""
h = a / 2
if not end:
return [(x, -h, z), (x, h, z)]
return [(x, -h, z + dz), (x, -h, z), (x, h, z), (x, h, z + dz)]
def bar_x(y, z, a, end, dz):
h = a / 2
if not end:
return [(-h, y, z), (h, y, z)]
return [(-h, y, z + dz), (-h, y, z), (h, y, z), (h, y, z + dz)]
def sched(m):
row = P["footing_rebar"]["schedule"][m]
nums = [float(t) for t in row[3:] if t.replace(".", "", 1).isdigit()]
return row, nums
def mm(v):
return v * 1000 if v < 100 else v
def main():
for o in list(doc.Objects):
oid = o.Attributes.GetUserString("id") or ""
if oid.startswith((f"{PID}-R-B", f"{PID}-RC-B")):
doc.Objects.Delete(o, True)
fr = P["footing_rebar"]
L = fr["layers"]
n = 0
a_y, a_x = fr["a_y"], fr["a_x"] # D3 a of Y-bars (B1-B4, B12) / X-bars (B5-B8, B11)
# bottom layer 1 (Y): B2 U (legs up) at the outermost positions and alternate from the next; B4 straight
pos = L["bottom_Y1"]["pos"]
edge = max(abs(x) for x in pos)
inner = [x for x in pos if abs(x) != edge]
for i, x in enumerate(pos):
mark = "B2" if abs(x) == edge or inner.index(x) % 2 == 0 else "B4"
z = FB + L["bottom_Y1"]["z"]
if mark == "B2":
c, dl = poly(bar_y(x, z, a_y, True, leg("B2")), MARK["B2"][0]), a_y + 2 * abs(leg("B2"))
else:
c, dl = poly(bar_y(x, z, a_y, False, 0), MARK["B4"][0]), a_y
add_bar(c, mark, f"x{x:+.0f}", dl, "D1 정면도 하면 점 z122 / D1·D2 '70@125=8.750'")
n += 1
pos = L["bottom_X2"]["pos"]
edge = max(abs(y) for y in pos)
inner = [y for y in pos if abs(y) != edge]
for y in pos:
mark = "B6" if abs(y) == edge or inner.index(y) % 2 == 0 else "B8"
z = FB + L["bottom_X2"]["z"]
if mark == "B6":
c, dl = poly(bar_x(y, z, a_x, True, leg("B6")), MARK["B6"][0]), a_x + 2 * abs(leg("B6"))
else:
c, dl = poly(bar_x(y, z, a_x, False, 0), MARK["B8"][0]), a_x
add_bar(c, mark, f"y{y:+.0f}", dl, "D1 단면도 하면 점 z178 / D1 '58@125=7.250'")
n += 1
for key, mark, axis, a in (("bottom_Y3", "B3", "Y", a_y), ("bottom_X4", "B7", "X", a_x),
("top_Y", "B1", "Y", a_y), ("top_X", "B5", "X", a_x)):
z, dz, dia = FB + L[key]["z"], leg(mark), MARK[mark][0]
for v in L[key]["pos"]:
pts = bar_y(v, z, a, True, dz) if axis == "Y" else bar_x(v, z, a, True, dz)
add_bar(poly(pts, dia), mark, f"{axis.lower()}{v:+.0f}", a + 2 * abs(dz),
f"D1 {'정면도' if axis == 'Y' else '단면도'} 점 z{L[key]['z']}")
n += 1
for key, mark, axis, a in (("mid_X", "B11", "X", a_x), ("mid_Y", "B12", "Y", a_y)):
for zs, vs in L[key]["levels"].items():
z = FB + float(zs)
dz = leg(mark)
for v in vs:
pts = bar_y(v, z, a, True, dz) if axis == "Y" else bar_x(v, z, a, True, dz)
add_bar(poly(pts, MARK[mark][0]), mark, f"z{float(zs):.0f}{axis.lower()}{v:+.0f}", a + 2 * abs(dz),
f"D1 {'정면도' if axis == 'Y' else '단면도'} 중앙 점 z{zs}")
n += 1
b10 = fr["b10"]
hx, hy = b10["size_x"] / 2, b10["size_y"] / 2
for k, zs in enumerate(b10["levels"]):
z = FB + zs
pts = [(-hx, 0, z), (-hx, -hy, z), (hx, -hy, z), (hx, hy, z), (-hx, hy, z), (-hx, 0.0, z)]
c = poly(pts, MARK["B10"][0])
add_bar(c, "B10", f"z{zs}", 2 * (b10["size_x"] + b10["size_y"]),
"D1 정면도 '150','9@200'; D3 B10 9.198x7.644 (closed, start/end at -X mid side)")
n += 1
b9 = fr["b9"]
rc = b9["c"] / math.pi # 180-degree hook developed length = c
for i, x in enumerate(b9["grid_x"]):
for j, y in enumerate(b9["grid_y"]):
if (i + j) % 2 != b9["checker_parity"]:
continue
z0, z1 = FB + b9["z0"], FB + b9["z0"] + b9["a"]
base = poly([(x + b9["b"], y, z0), (x, y, z0), (x, y, z1)], MARK["B9"][0])
top = rg.ArcCurve(rg.Arc(rg.Point3d(x, y, z1), rg.Point3d(x + rc, y, z1 + rc),
rg.Point3d(x + 2 * rc, y, z1)))
c = rg.Curve.JoinCurves([base, top], TOL)[0]
add_bar(c, "B9", f"x{x:+.0f}y{y:+.0f}", b9["a"] + b9["b"] + b9["c"],
"D1 EREC grid @250 checker (i+j even) [추론]; count 493 [역산]")
n += 1
doc.Views.Redraw()
print(json.dumps({"footing_bars": n}))
main()
@@ -0,0 +1,138 @@
"""G3 stage, form "mats" (Ulsan P1, 2026-10-13): footing rebar as explicit bar groups, perimeter hoops and
vertical bars, from params `footing_rebar` (centreline + pipe solid per bar).
Run inside Rhino 8 via runner (globals PARAMS_PATH) after the G1 builder. z values are above the footing bottom.
params footing_rebar:
groups [{mark, axis "X"|"Y", z, pos [...], a, leg (signed: + up / - down / 0), lap, src}]
a bar along `axis` of length a centred on the footing, at `pos` across, with end legs
design length = a + 2|leg| + lap (lap only adds length)
hoops {mark, half, levels [...], design_length}: closed square hoops of centreline half-width `half`
verticals {mark, grid_x [...], grid_y [...], parity, z0, a, leg}: vertical bars on a checkerboard of the grid,
legs of length `leg` towards +`leg_axis` ("X"|"Y") at both ends (shape I with b, SD400 catalogue)
Optional (Ulsan P2, 2026-10-15; absent -> unchanged):
footing.rot_deg all bars are built in the foundation's own axes and turned counter-clockwise about the pier centre
hoops.half_x / half_y rectangular hoop (default: square `half`)
verticals.points [[x, y], ...] explicit positions instead of the checkerboard; leg_bottom / leg_top leg lengths per end
(default `leg`); design length = a + leg_bottom + leg_top
"""
import json
import math
import Rhino
import Rhino.Geometry as rg
import System
from Rhino.DocObjects import ObjectAttributes
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
FB = P["footing"]["z_bottom"]
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items()}
DIA = {d: float(d[1:]) for d, _ in MARK.values()}
CL_RGB = tuple(P["display"]["centreline_colour"])
FT_RGB = tuple(P["display"]["footing_layer_colour"])
def layer(path, color):
parent = -1
parts = path.split("::")
for i in range(1, len(parts) + 1):
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
if idx < 0:
lay = Rhino.DocObjects.Layer()
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
if parent >= 0:
lay.ParentLayerId = doc.Layers[parent].Id
idx = doc.Layers.Add(lay)
parent = idx
return parent
ROT = math.radians(P["footing"].get("rot_deg", 0.0))
def turn(pts):
"""Foundation axes -> pier axes (counter-clockwise ROT about the origin)."""
if not ROT:
return pts
c, s = math.cos(ROT), math.sin(ROT)
return [(x * c - y * s, x * s + y * c, z) for x, y, z in pts]
def min_r(dia):
d = DIA[dia]
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2
def poly(pts, dia):
pl = rg.PolylineCurve([rg.Point3d(*p) for p in pts])
if len(pts) < 3:
return pl
return rg.Curve.CreateFilletCornersCurve(pl, min_r(dia), TOL, math.radians(1)) or pl
def add_bar(curve, mark, occ, design_len, src):
dia, shape = MARK[mark]
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
if not pipes or not pipes[0].IsSolid:
raise RuntimeError(f"{mark} {occ}: pipe failed")
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-FT", anchor_hosts="",
design_length=f"{design_len:.3f}", coupler_length="0.000", src=src)
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
(pipes[0], layer(f"{PID}::철근::기초::{mark}", FT_RGB), f"{PID}-R")):
attr = ObjectAttributes()
attr.LayerIndex = lay
oid = f"{prefix}-{mark}-{occ}"
attr.Name = oid
attr.SetUserString("id", oid)
for k, v in common.items():
attr.SetUserString(k, v)
gid = doc.Objects.AddBrep(geo, attr) if isinstance(geo, rg.Brep) else doc.Objects.AddCurve(geo, attr)
if gid == System.Guid.Empty:
raise RuntimeError(f"{oid}: add failed")
def main():
for o in list(doc.Objects):
oid = o.Attributes.GetUserString("id") or ""
if oid.startswith((f"{PID}-R-B", f"{PID}-RC-B")):
doc.Objects.Delete(o, True)
fr = P["footing_rebar"]
n = 0
for g in fr["groups"]:
h, z, leg = g["a"] / 2, FB + g["z"], g.get("leg", 0)
for v in g["pos"]:
if g["axis"] == "Y":
ends = [(v, -h, z), (v, h, z)]
else:
ends = [(-h, v, z), (h, v, z)]
pts = ends if not leg else [(ends[0][0], ends[0][1], z + leg), ends[0], ends[1], (ends[1][0], ends[1][1], z + leg)]
add_bar(poly(turn(pts), MARK[g["mark"]][0]), g["mark"], f"{g['axis'].lower()}{v:+.0f}",
g["a"] + 2 * abs(leg) + g.get("lap", 0), g.get("src", ""))
n += 1
hp = fr["hoops"]
sx, sy = hp.get("half_x", hp.get("half")), hp.get("half_y", hp.get("half"))
for zz in hp["levels"]:
z = FB + zz
pts = [(-sx, 0, z), (-sx, -sy, z), (sx, -sy, z), (sx, sy, z), (-sx, sy, z), (-sx, 0.0, z)]
add_bar(poly(turn(pts), MARK[hp["mark"]][0]), hp["mark"], f"z{zz:.0f}", hp["design_length"], hp.get("src", ""))
n += 1
vt = fr["verticals"]
if "points" in vt:
where = [tuple(q) for q in vt["points"]]
else:
where = [(x, y) for i, x in enumerate(vt["grid_x"]) for j, y in enumerate(vt["grid_y"]) if (i + j) % 2 == vt["parity"]]
lb, lt = vt.get("leg_bottom", vt.get("leg", 0)), vt.get("leg_top", vt.get("leg", 0))
ux, uy = (1, 0) if vt.get("leg_axis", "X") == "X" else (0, 1)
for x, y in where:
z0, z1 = FB + vt["z0"], FB + vt["z0"] + vt["a"]
pts = [(x + ux * lb, y + uy * lb, z0), (x, y, z0), (x, y, z1), (x + ux * lt, y + uy * lt, z1)]
add_bar(poly(turn(pts), MARK[vt["mark"]][0]), vt["mark"], f"x{x:+.0f}y{y:+.0f}", vt["a"] + lb + lt, vt.get("src", ""))
n += 1
doc.Views.Redraw()
print(json.dumps({"footing_bars": n}))
main()
@@ -0,0 +1,34 @@
"""Negative test helper: move selected objects of the open doc (inside Rhino 8, via runner).
Globals: __rhino_doc__, PIER_ID, MOVES = [{"id": "BS08"} | {"mark": "P2"}, "d": [dx, dy, dz]}, ...],
optional UNDO (move back by -d). A `mark` selector moves the first occurrence (sorted) of that mark,
centreline and pipe together. Prints what was moved; raises if a selector matches nothing.
"""
import json
import Rhino.Geometry as rg
from Rhino.DocObjects import ObjectEnumeratorSettings
doc = __rhino_doc__
s = ObjectEnumeratorSettings()
s.HiddenObjects = True
s.LockedObjects = True
objs = list(doc.Objects.GetObjectList(s))
sign = -1 if globals().get("UNDO") else 1
moved = []
for mv in MOVES:
if "id" in mv:
sel = [o for o in objs if o.Attributes.GetUserString("id") == f"{PIER_ID}-{mv['id']}"]
else:
cand = [o for o in objs if o.Attributes.GetUserString("mark") == mv["mark"]
and (o.Attributes.GetUserString("id") or "").startswith(f"{PIER_ID}-R")]
first = sorted({o.Attributes.GetUserString("occurrence") for o in cand})[0] if cand else None
sel = [o for o in cand if o.Attributes.GetUserString("occurrence") == first]
if not sel:
raise RuntimeError(f"perturb: nothing matches {mv}")
dx, dy, dz = (sign * v for v in mv["d"])
xf = rg.Transform.Translation(dx, dy, dz)
for o in sel:
doc.Objects.Transform(o.Id, xf, True)
moved.append(o.Attributes.GetUserString("id"))
print(json.dumps({"moved": moved, "undo": sign < 0}))
@@ -0,0 +1,53 @@
"""Colour rebar pipe solids by nominal diameter (run inside Rhino 8 via runner after the rebar stages).
Globals: PARAMS_PATH. Ported from Code/교각모델링/apply_rebar_colors.py (Ungok P1, model v0.1.09).
Rule (context-migration/디자인지침_경로.md, R8 criteria 2026-10-07): same diameter -> same colour
regardless of steel grade (S = SD500, H = SD400). Centrelines keep their layer colour.
Prints a check: per-diameter object count and colour, and any pipe whose colour does not match.
"""
import json
from collections import Counter
from Rhino.DocObjects import ObjectColorSource
from System.Drawing import Color
doc = __rhino_doc__
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
PID = P["pier_id"]
PALETTE = {int(k): tuple(v) for k, v in P["display"]["rebar_palette"].items()}
def main():
done, bad = Counter(), []
for o in list(doc.Objects):
oid = o.Attributes.GetUserString("id") or ""
if not oid.startswith(f"{PID}-R-"):
continue
dia = int(o.Attributes.GetUserString("dia")[1:]) # 'S32' / 'H19' -> 32 / 19
if dia not in PALETTE:
bad.append((oid, "no palette entry"))
continue
attr = o.Attributes.Duplicate()
attr.ObjectColor = Color.FromArgb(*PALETTE[dia])
attr.ColorSource = ObjectColorSource.ColorFromObject
doc.Objects.ModifyAttributes(o, attr, True)
done[dia] += 1
# check (re-read objects)
seen = {}
for o in doc.Objects:
oid = o.Attributes.GetUserString("id") or ""
if oid.startswith(f"{PID}-R-"):
dia = int(o.Attributes.GetUserString("dia")[1:])
c = o.Attributes.ObjectColor
ok = o.Attributes.ColorSource == ObjectColorSource.ColorFromObject and (c.R, c.G, c.B) == PALETTE[dia]
if not ok:
bad.append((oid, "colour mismatch"))
seen.setdefault(dia, set()).add((c.R, c.G, c.B))
doc.Views.Redraw()
print(json.dumps(dict(coloured=dict(done), colours_per_dia={k: list(v) for k, v in seen.items()},
distinct_colours=len({tuple(PALETTE[k]) for k in seen}), mismatches=bad[:10],
mismatch_count=len(bad))))
main()
@@ -0,0 +1,91 @@
"""Generic rebar stage (Ulsan P1 coping, 2026-10-13): bars given as explicit centreline polylines in params.
Run inside Rhino 8 via runner (globals PARAMS_PATH). Used as the G4 builder (params["coping_rebar"]).
params coping_rebar:
host object id suffix of the host concrete (e.g. "CP")
group layer group under 철근:: (e.g. "코핑")
bars [{mark, occ, pts [[x, y, z], ...], r (corner fillet radius), design_length, src}]
a closed bar repeats its first point at the end (start it mid-side so no corner sits at the seam)
Ids: <pier>-R-<mark>-<occ> (pipe) and <pier>-RC-<mark>-<occ> (centreline).
Section by gate (2026-10-14, Okseong P1): GATE "G3" -> params footing_rebar, otherwise coping_rebar.
"""
import json
import math
import Rhino
import Rhino.Geometry as rg
import System
from Rhino.DocObjects import ObjectAttributes
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
SEC = P["footing_rebar" if globals().get("GATE") == "G3" else "coping_rebar"]
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items()}
CL_RGB = tuple(P["display"]["centreline_colour"])
LAY_RGB = tuple(P["display"]["coping_layer_colour"])
def layer(path, color):
parent = -1
parts = path.split("::")
for i in range(1, len(parts) + 1):
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
if idx < 0:
lay = Rhino.DocObjects.Layer()
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
if parent >= 0:
lay.ParentLayerId = doc.Layers[parent].Id
idx = doc.Layers.Add(lay)
parent = idx
return parent
def centreline(pts, r):
pl = rg.PolylineCurve([rg.Point3d(*p) for p in pts])
if len(pts) < 3 or r <= 0:
return pl
c = rg.Curve.CreateFilletCornersCurve(pl, r, TOL, math.radians(1))
if c is None:
raise RuntimeError(f"fillet failed (r {r})")
return c
def add_bar(b):
mark, occ = b["mark"], b["occ"]
dia, shape = MARK[mark]
curve = centreline(b["pts"], b.get("r", 0))
pipes = rg.Brep.CreatePipe(curve, float(dia[1:]) / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
if not pipes or not pipes[0].IsSolid:
raise RuntimeError(f"{mark} {occ}: pipe failed")
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=f"{PID}-{SEC['host']}", anchor_hosts="",
design_length=f"{b['design_length']:.3f}", coupler_length="0.000", src=b.get("src", ""))
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
(pipes[0], layer(f"{PID}::철근::{SEC['group']}::{mark}", LAY_RGB), f"{PID}-R")):
attr = ObjectAttributes()
attr.LayerIndex = lay
oid = f"{prefix}-{mark}-{occ}"
attr.Name = oid
attr.SetUserString("id", oid)
for k, v in common.items():
attr.SetUserString(k, v)
gid = doc.Objects.AddBrep(geo, attr) if isinstance(geo, rg.Brep) else doc.Objects.AddCurve(geo, attr)
if gid == System.Guid.Empty:
raise RuntimeError(f"{oid}: add failed")
def main():
marks = {b["mark"] for b in SEC["bars"]}
for o in list(doc.Objects):
oid = o.Attributes.GetUserString("id") or ""
if oid.startswith((f"{PID}-R-", f"{PID}-RC-")) and o.Attributes.GetUserString("mark") in marks:
doc.Objects.Delete(o, True)
for b in SEC["bars"]:
add_bar(b)
doc.Views.Redraw()
print(json.dumps({"bars": len(SEC["bars"]), "marks": len(marks)}))
main()
@@ -0,0 +1,180 @@
"""G3 stage, form "shaft cage" (Ramp-A P1, 2026-10-13): drilled-shaft reinforcement cage (no footing). Straight main
bars in two pieces joined by couplers (upper / lower mark), lapped half-ring hoops with 135-deg hooks, X / Y cross
ties, bottom U bars in vertical planes through the centre, and the anti-uplift plate under them.
Run inside Rhino 8 via runner (globals PARAMS_PATH) after the G1 / G2 builders. Units mm, Z = EL x 1000.
params shaft_rebar:
main {count, start_deg, radius, pieces: [{mark, z0, z1}, ...] bottom to top, coupler_length}
hoops {mark, radius, levels, a, lap, hook, join_deg [first, second]} joins alternate per level (S5)
ties {mark, levels, straight, leg, hook_tail, y_lift} as column_rebar_straight.py (S5)
u_bars {mark, count, z_bottom, half_bottom, half_top, leg_top_z, top_z, bend_r} one U per plane at main-bar angle
i (i < count): bottom +-half_bottom at z_bottom, sloped legs to +-half_top at leg_top_z, vertical to top_z (S3)
plate {size, thickness, z_top, colour} square, centred (S4)
design_lengths {mark: schedule-style original length per piece}
Bars below the pile segment boundary are hosted by the lower pile segment, above it by the upper one.
"""
import json
import math
import Rhino
import Rhino.Geometry as rg
from Rhino.DocObjects import ObjectAttributes, ObjectColorSource
from System.Drawing import Color
P = json.load(open(PARAMS_PATH, encoding="utf-8"))
doc = __rhino_doc__
TOL = 0.01
PID = P["pier_id"]
MARK = {m: (v["dia"], v["shape"]) for m, v in P["rebar_marks"].items()}
DIA = {d: float(d[1:]) for d, _ in MARK.values()}
CL_RGB = tuple(P["display"]["centreline_colour"])
SEGS = P["pile"]["segments"]
def layer(path, color):
parent = -1
parts = path.split("::")
for i in range(1, len(parts) + 1):
idx = doc.Layers.FindByFullPath("::".join(parts[:i]), -1)
if idx < 0:
lay = Rhino.DocObjects.Layer()
lay.Name, lay.Color = parts[i - 1], Color.FromArgb(*color)
if parent >= 0:
lay.ParentLayerId = doc.Layers[parent].Id
idx = doc.Layers.Add(lay)
parent = idx
return parent
def min_bend_radius(dia_name):
d = DIA[dia_name]
return ((2 * d if d <= 16 else 3.5 * d) + d) / 2
def host(z):
for i, s in enumerate(SEGS, 1):
if s["z0"] <= z <= s["z1"]:
return f"{PID}-PL{i:02d}"
return ""
def add_bar(curve, mark, occ, design_len, src, coupler=0.0):
dia, shape = MARK[mark]
pipes = rg.Brep.CreatePipe(curve, DIA[dia] / 2, False, rg.PipeCapMode.Flat, False, TOL, math.radians(1))
if not pipes or not pipes[0].IsSolid:
raise RuntimeError(f"{mark} {occ}: pipe failed")
bb = curve.GetBoundingBox(True)
common = dict(mark=mark, dia=dia, shape=shape, occurrence=occ, host=host((bb.Min.Z + bb.Max.Z) / 2),
anchor_hosts="", design_length=f"{design_len:.3f}", coupler_length=f"{coupler:.3f}", src=src)
for geo, lay, prefix in ((curve, layer(f"{PID}::철근::중심선", CL_RGB), f"{PID}-RC"),
(pipes[0], layer(f"{PID}::철근::말뚝::{mark}", (110, 110, 110)), f"{PID}-R")):
attr = ObjectAttributes()
attr.LayerIndex = lay
oid = f"{prefix}-{mark}-{occ}"
attr.Name = oid
attr.SetUserString("id", oid)
for k, v in common.items():
attr.SetUserString(k, v)
gid = doc.Objects.AddBrep(geo, attr) if isinstance(geo, rg.Brep) else doc.Objects.AddCurve(geo, attr)
if gid == __import__("System").Guid.Empty:
raise RuntimeError(f"{oid}: add failed")
def pt(r, deg, z):
t = math.radians(deg)
return rg.Point3d(r * math.cos(t), r * math.sin(t), z)
def hooked_arc(r, a0, a1, z, hook, bend_r):
"""Arc a0 -> a1 (deg, ccw) with inward 135-deg hooks at both ends, fillet bend_r (as column_rebar_ring.py)."""
arc = rg.ArcCurve(rg.Arc(pt(r, a0, z), pt(r, (a0 + a1) / 2, z), pt(r, a1, z)))
pieces = []
for at_start in (True, False):
p = arc.PointAtStart if at_start else arc.PointAtEnd
t = -arc.TangentAtStart if at_start else arc.TangentAtEnd
n = rg.Vector3d(-p.X, -p.Y, 0)
n.Unitize()
d = t * math.cos(math.radians(135)) + n * math.sin(math.radians(135))
d.Unitize()
pieces.append(rg.LineCurve(p, p + d * hook))
joined = rg.Curve.JoinCurves([pieces[0], arc, pieces[1]], TOL)[0]
return rg.Curve.CreateFilletCornersCurve(joined, bend_r, TOL, math.radians(1)) or joined
def tie_curve(axis, z, half, leg, tail, flip):
"""As column_rebar_straight.py: straight part on the X / Y diameter, 90-deg leg down at one end, 135-deg hook."""
u = rg.Vector3d(1, 0, 0) if axis == 0 else rg.Vector3d(0, 1, 0)
s = -1 if flip else 1
a, b = rg.Point3d(0, 0, z) - u * (s * half), rg.Point3d(0, 0, z) + u * (s * half)
d = -u * s * math.cos(math.radians(45)) + rg.Vector3d(0, 0, -1) * math.sin(math.radians(45))
return rg.PolylineCurve([a + rg.Vector3d(0, 0, -leg), a, b, b + d * tail])
def main():
for o in list(doc.Objects):
oid = o.Attributes.GetUserString("id") or ""
if oid.startswith((f"{PID}-PT",)) or (oid.startswith((f"{PID}-R-", f"{PID}-RC-"))
and (o.Attributes.GetUserString("mark") or "") in SR_MARKS):
doc.Objects.Delete(o, True)
DL = SR["design_lengths"]
m, n = SR["main"], 0
for i in range(m["count"]):
th = m["start_deg"] + i * 360.0 / m["count"]
for pc in m["pieces"]:
c = rg.LineCurve(pt(m["radius"], th, pc["z0"]), pt(m["radius"], th, pc["z1"]))
add_bar(c, pc["mark"], f"{i + 1:02d}", DL[pc["mark"]],
f"angle {th:.4f} deg; D4 단면 G-G·J-J 61개; r {m['radius']} (S2); EL {pc['z0'] / 1000:.3f}~{pc['z1'] / 1000:.3f}; "
f"커플러 이음 (S6)", coupler=m["coupler_length"])
n += 1
h = SR["hoops"]
r, ext = h["radius"], math.degrees(h["lap"] / 2 / h["radius"])
for j, z in enumerate(h["levels"]):
a0 = h["join_deg"][j % 2]
for q, s in enumerate((a0, a0 + 180)):
c = hooked_arc(r, s - ext, s + 180 + ext, z, h["hook"], min_bend_radius(MARK[h["mark"]][0]))
add_bar(c, h["mark"], f"L{j + 1:03d}{'ab'[q]}", DL[h["mark"]],
f"EL {z / 1000:.3f}; r {r} (S1); 겹이음 {h['lap']:.0f} at {a0}/{a0 + 180} deg (S5 [추론])")
n += 1
t = SR["ties"]
rb = min_bend_radius(MARK[t["mark"]][0])
for j, z in enumerate(t["levels"]):
for axis, name in ((0, "X"), (1, "Y")):
zt = z + (t["y_lift"] if axis == 1 else 0)
pl = tie_curve(axis, zt, t["straight"] / 2, t["leg"], t["hook_tail"], flip=(j % 2 == 1))
c = rg.Curve.CreateFilletCornersCurve(pl, rb, TOL, math.radians(1)) or pl
add_bar(c, t["mark"], f"L{j + 1:02d}{name}", DL[t["mark"]],
f"EL {zt / 1000:.3f}; {name} 방향 (S5 [추론])")
n += 1
u = SR["u_bars"]
for i in range(u["count"]):
th = m["start_deg"] + i * 360.0 / m["count"]
e = rg.Vector3d(math.cos(math.radians(th)), math.sin(math.radians(th)), 0)
P0 = lambda s, z: rg.Point3d(0, 0, z) + e * s
pl = rg.PolylineCurve([P0(-u["half_top"], u["top_z"]), P0(-u["half_top"], u["leg_top_z"]),
P0(-u["half_bottom"], u["z_bottom"]), P0(u["half_bottom"], u["z_bottom"]),
P0(u["half_top"], u["leg_top_z"]), P0(u["half_top"], u["top_z"])])
c = rg.Curve.CreateFilletCornersCurve(pl, u["bend_r"], TOL, math.radians(1)) or pl
add_bar(c, u["mark"], f"{i + 1:02d}", DL[u["mark"]],
f"plane {th:.4f} deg through the centre (S3 [추론]); bottom EL {u['z_bottom'] / 1000:.3f}; c {u['top_z'] - u['leg_top_z']:.0f} welded to P1-2")
n += 1
pt_ = SR["plate"]
a = pt_["size"] / 2
box = rg.Box(rg.BoundingBox(-a, -a, pt_["z_top"] - pt_["thickness"], a, a, pt_["z_top"])).ToBrep()
attr = ObjectAttributes()
attr.LayerIndex = layer(f"{PID}::철근::말뚝::PLATE", tuple(pt_["colour"]))
attr.ObjectColor = Color.FromArgb(*pt_["colour"])
attr.ColorSource = ObjectColorSource.ColorFromObject
oid = f"{PID}-PT01"
attr.Name = oid
for k, v in (("id", oid), ("role", "anti-uplift plate"), ("host", host(pt_["z_top"])),
("src", f"D4 떠오름 방지 PLATE {pt_['size']:.0f}x{pt_['size']:.0f}x{pt_['thickness']:.0f}; U형 바닥 아래 (S4 [추론])")):
attr.SetUserString(k, v)
if doc.Objects.AddBrep(box, attr) == __import__("System").Guid.Empty:
raise RuntimeError("plate: add failed")
doc.Views.Redraw()
print(json.dumps({"bars": n, "plate": 1}))
SR = P["shaft_rebar"]
SR_MARKS = {pc["mark"] for pc in SR["main"]["pieces"]} | {SR[k]["mark"] for k in ("hoops", "ties", "u_bars")}
main()