Files
Pier_v1/Code/교각모델링앱/engine/rhino/concrete_tapered.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

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