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