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