- 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>
268 lines
14 KiB
Python
268 lines
14 KiB
Python
"""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()
|