"""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(, encoding="utf-8").read(), {"__rhino_doc__": __rhino_doc__, "PARAMS_PATH": }) 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()