Files
Pier_v1/Code/교각모델링앱/engine/rhino/concrete.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

191 lines
8.6 KiB
Python

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