- 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>
194 lines
8.6 KiB
Python
194 lines
8.6 KiB
Python
"""Stage 3: build the Ungok Bridge P1 pier concrete model in Rhino from params.json.
|
|
|
|
Run inside Rhino 8 (Python 3) with `__rhino_doc__` injected, e.g. via
|
|
exec(open(<this file>, encoding="utf-8").read(),
|
|
{"__rhino_doc__": __rhino_doc__, "PARAMS_PATH": <params.json>})
|
|
Only params.json is read; no drawing-name branching. Units: mm, Z = EL x 1000.
|
|
"""
|
|
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
|
|
|
|
LAYERS = {
|
|
"lean": ("P1::버림콘크리트", Color.FromArgb(160, 160, 160)),
|
|
"footing": ("P1::기초", Color.FromArgb(120, 150, 200)),
|
|
"column": ("P1::기둥", Color.FromArgb(200, 140, 90)),
|
|
"coping": ("P1::코핑", Color.FromArgb(210, 180, 80)),
|
|
"pile": ("P1::말뚝", Color.FromArgb(110, 110, 110)),
|
|
"seat": ("P1::받침대좌", Color.FromArgb(190, 90, 90)),
|
|
"pad": ("P1::받침패드", Color.FromArgb(90, 160, 90)),
|
|
}
|
|
|
|
|
|
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"]
|
|
by_no = {b["no"]: b for b in bs["bearings"]}
|
|
for b in bs["bearings"]:
|
|
shift = (b["girder_line"] - 5) * 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"P1-BS{b['no']:02d}",
|
|
f"bearing pedestal h1 NO.{b['no']} girder {b['girder_line']} {b['row']}{tag}", "40MPa",
|
|
"D1 평면도, D1 단면도, D2 ELEVATION TABLE")
|
|
cx, cy = b["centre"]
|
|
bottom = rotated_rect(cx, cy, b["A"] + 200, b["B"] + 200, b["rotation_deg"], z1)
|
|
top = rotated_rect(cx, cy, b["A"] + 100, b["B"] + 100, b["rotation_deg"], z1 + b["h2"])
|
|
add(frustum(bottom, top), "pad", f"P1-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"]
|
|
pile_xy = [(-((pl["count_x"] - 1) * pl["pitch"]) / 2 + i * pl["pitch"],
|
|
-((pl["count_y"] - 1) * pl["pitch"]) / 2 + j * pl["pitch"])
|
|
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", "P1-LC", "lean concrete", "15MPa", "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", "P1-FT", "footing", "27MPa",
|
|
"D1 정면도, D1 단면도, D2 기초평면도")
|
|
add(cylinder(0, 0, ft_top, col_top, cl["diameter"] / 2), "column", "P1-CL", "column", "40MPa",
|
|
"D1 정면도, D1 단면도, D1 단면 C-C/D-D/E-E, D2 기초평면도")
|
|
cop, centers = coping_brep(cp)
|
|
add(cop, "coping", "P1-CP", "coping", "40MPa", "D1 평면도, D1 정면도, D1 단면도, D1 단면 A-A/B-B")
|
|
for n, b in enumerate(piles, 1):
|
|
add(b, "pile", f"P1-PL{n:02d}", "composite pile D600 L24.4m", "-", "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()
|