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

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

206 lines
11 KiB
Python

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