스킬 · 기타

라이노 8 건축 모델링·검사

라이노 8에서 파빌리온·구조 프레임·입면 루버 같은 건축 개념 모델을 만들고 모델 품질을 검사하는 원작 스킬. MIT 라이선스.

0번 설치됨
스킬스크립트 포함승인됨원작 leoyang1984 · MIT · 올림 feeeldstudy · 판 1Claude · ChatGPT · Codex · Cursor
rhino-architectural-modeling/assets/parametric-pavilion-example.py
"""Parametric Timber Pavilion — RhinoCommon Python 3.Runs through Rhino MCP run_python with __rhino_doc__ injected."""import Rhinoimport Rhino.Geometry as rgimport Rhino.DocObjects as rdoimport System.Drawingimport mathdoc = __rhino_doc__  # injected — do not use RhinoDoc.ActiveDoc# ── Parameters ─────────────────────────────────────────────────PARAMS = {    "length": 12000,          # mm, X axis    "width": 8000,            # mm, Y axis    "frame_count": 7,         # portal frames including both ends    "eave_height": 3000,      # mm, Z of eave    "ridge_height": 4500,     # mm, Z of ridge    "column_section": (200, 200),   # (wx, wy) mm    "beam_section": (200, 300),     # (wx, wy) mm — wy is vertical dim    "louver_count_per_side": 10,    "louver_angle_deg": 30,    "entrance_width": 2400,   # mm clear opening    "entrance_offset": 0,     # mm from facade centre along Y}p = PARAMS# ── Derived ────────────────────────────────────────────────────frame_spacing = p["length"] / (p["frame_count"] - 1)mid_y = p["width"] / 2.0ent_half = p["entrance_width"] / 2.0ent_start_y = mid_y + p["entrance_offset"] - ent_halfent_end_y = mid_y + p["entrance_offset"] + ent_halfcw = p["column_section"][0]cd = p["column_section"][1]bw = p["beam_section"][0]bd = p["beam_section"][1]rafter_dy = mid_y        # horizontal run from outer eave edge to ridge centrerafter_dz = p["ridge_height"] - p["eave_height"]  # vertical riserafter_len = math.sqrt(rafter_dy**2 + rafter_dz**2)rafter_vec_L = rg.Vector3d(0, rafter_dy, rafter_dz)rafter_vec_L.Unitize()rafter_vec_R = rg.Vector3d(0, -rafter_dy, rafter_dz)rafter_vec_R.Unitize()louver_angle_rad = math.radians(p["louver_angle_deg"])# ── Colours ────────────────────────────────────────────────────COL = {    "Columns":  System.Drawing.Color.FromArgb(139, 90, 43),    "Beams":    System.Drawing.Color.FromArgb(160, 120, 60),    "Rafters":  System.Drawing.Color.FromArgb(180, 140, 80),    "Louvers":  System.Drawing.Color.FromArgb(200, 170, 120),    "Reference": System.Drawing.Color.FromArgb(100, 100, 100),}# ── Helpers ────────────────────────────────────────────────────def _layer(path):    idx = doc.Layers.FindByFullPath(path, True)    if idx >= 0:        return idx    return doc.Layers.AddPath(path, COL.get(path.split("::")[-1],                                            System.Drawing.Color.Black))def _attrs(layer_idx, name):    oa = rdo.ObjectAttributes()    oa.LayerIndex = layer_idx    oa.Name = name    return oadef _add_brep(brep, layer_idx, name):    return doc.Objects.AddBrep(brep, _attrs(layer_idx, name))def _add_extrusion(ext, layer_idx, name):    return doc.Objects.AddExtrusion(ext, _attrs(layer_idx, name))def _is_closed_solid(obj_id):    rh_obj = doc.Objects.Find(obj_id)    if rh_obj is None:        return False    geo = rh_obj.Geometry    if geo is None:        return False    b = geo if isinstance(geo, rg.Brep) else None    if b is None:        try:            b = geo.ToBrep()        except Exception:            return False    return b.IsValid and b.IsSoliddef _box_brep(x0, y0, z0, x1, y1, z1):    bb = rg.BoundingBox(rg.Point3d(x0, y0, z0), rg.Point3d(x1, y1, z1))    return rg.Brep.CreateFromBox(bb)def _frame_x_bounds(frame_index, section):    """Keep end-frame solids inside the 0..length design envelope."""    fx = frame_index * frame_spacing    if frame_index == 0:        return fx, 0.0, float(section)    if frame_index == p["frame_count"] - 1:        return fx, float(p["length"] - section), float(p["length"])    return fx, fx - section / 2.0, fx + section / 2.0def _sloped_member(frame_index, start, direction, depth, side):    """Create a capped rectangular member whose top line follows direction."""    fx, x0, x1 = _frame_x_bounds(frame_index, cw)    x_axis = rg.Vector3d.XAxis    if side == "L":        y_axis = rg.Vector3d(0, direction.Z, -direction.Y)        depth_interval = rg.Interval(0, depth)    else:        y_axis = rg.Vector3d(0, direction.Z, -direction.Y)        depth_interval = rg.Interval(-depth, 0)    y_axis.Unitize()    plane = rg.Plane(start, x_axis, y_axis)    box = rg.Box(        plane,        rg.Interval(x0 - fx, x1 - fx),        depth_interval,        rg.Interval(0, rafter_len),    )    return box.ToBrep()# ── Idempotent delete ──────────────────────────────────────────to_delete = []for obj in doc.Objects:    if obj.Name and obj.Name.startswith("PAV_"):        to_delete.append(obj.Id)if to_delete:    doc.Objects.Delete(to_delete, True)# ── Units and layers ───────────────────────────────────────────doc.ModelUnitSystem = Rhino.UnitSystem.Millimeterslayer_cols = _layer("PAVILION::Columns")layer_beams = _layer("PAVILION::Beams")layer_rafters = _layer("PAVILION::Rafters")layer_louvers = _layer("PAVILION::Louvers")layer_ref = _layer("PAVILION::Reference")# ── Counters ───────────────────────────────────────────────────counts = {"Columns": 0, "Beams": 0, "Rafters": 0, "Louvers": 0, "Reference": 0}semantic_counts = {"TieBeams": 0, "Longitudinal": 0}ids = []# ── Portal frames ──────────────────────────────────────────────for i in range(p["frame_count"]):    fx = i * frame_spacing    _, col_x0, col_x1 = _frame_x_bounds(i, cw)    _, beam_x0, beam_x1 = _frame_x_bounds(i, bw)    # Left column    col_brep = _box_brep(col_x0, 0, 0,                         col_x1, cd, p["eave_height"])    ids.append(_add_brep(col_brep, layer_cols, f"PAV_col_L_{i:02d}"))    counts["Columns"] += 1    # Right column    col_brep = _box_brep(col_x0, p["width"] - cd, 0,                         col_x1, p["width"], p["eave_height"])    ids.append(_add_brep(col_brep, layer_cols, f"PAV_col_R_{i:02d}"))    counts["Columns"] += 1    # One overhead tie beam per frame; the entrance remains clear below it.    beam_brep = _box_brep(beam_x0, 0, p["eave_height"] - bd,                          beam_x1, p["width"], p["eave_height"])    ids.append(_add_brep(beam_brep, layer_beams,                         f"PAV_tie_beam_{i:02d}"))    counts["Beams"] += 1    semantic_counts["TieBeams"] += 1    # Left rafter — top line runs Y+ from eave to ridge.    rafter = _sloped_member(        i, rg.Point3d(fx, 0, p["eave_height"]), rafter_vec_L, bd, "L"    )    ids.append(_add_brep(rafter, layer_rafters, f"PAV_rafter_L_{i:02d}"))    counts["Rafters"] += 1    # Right rafter — top line runs Y- from eave to ridge.    rafter = _sloped_member(        i, rg.Point3d(fx, p["width"], p["eave_height"]),        rafter_vec_R, bd, "R"    )    ids.append(_add_brep(rafter, layer_rafters, f"PAV_rafter_R_{i:02d}"))    counts["Rafters"] += 1# ── Longitudinal members: ridge ────────────────────────────────ridge_brep = _box_brep(0, mid_y - bw / 2, p["ridge_height"] - bd,                       p["length"], mid_y + bw / 2, p["ridge_height"])ids.append(_add_brep(ridge_brep, layer_beams, "PAV_ridge_beam"))counts["Beams"] += 1semantic_counts["Longitudinal"] += 1# ── Longitudinal members: eaves ────────────────────────────────eave_brep = _box_brep(0, cd / 2 - bw / 2, p["eave_height"] - bd / 2,                      p["length"], cd / 2 + bw / 2, p["eave_height"] + bd / 2)ids.append(_add_brep(eave_brep, layer_beams, "PAV_eave_beam_L"))counts["Beams"] += 1semantic_counts["Longitudinal"] += 1eave_brep = _box_brep(0, p["width"] - cd / 2 - bw / 2, p["eave_height"] - bd / 2,                      p["length"], p["width"] - cd / 2 + bw / 2, p["eave_height"] + bd / 2)ids.append(_add_brep(eave_brep, layer_beams, "PAV_eave_beam_R"))counts["Beams"] += 1semantic_counts["Longitudinal"] += 1# ── Louvers ────────────────────────────────────────────────────louvers_left_y_range = (0, ent_start_y)louvers_right_y_range = (ent_end_y, p["width"])louver_w = 150   # blade width mmlouver_t = 10    # blade thickness mmdef _build_louver_set(y0, y1, side_label, y_sign):    span = y1 - y0    for j in range(p["louver_count_per_side"]):        ly = y0 + span * (j + 0.5) / p["louver_count_per_side"]        depth_axis = rg.Vector3d(            math.cos(louver_angle_rad),            y_sign * math.sin(louver_angle_rad),            0,        )        width_axis = rg.Vector3d(            -y_sign * math.sin(louver_angle_rad),            math.cos(louver_angle_rad),            0,        )        # Shift inward so a centred blade thickness never crosses X=0.        origin = rg.Point3d(abs(width_axis.X) * louver_t / 2.0, ly, 0)        louver_plane = rg.Plane(origin, depth_axis, width_axis)        box = rg.Box(            louver_plane,            rg.Interval(0, louver_w),            rg.Interval(-louver_t / 2.0, louver_t / 2.0),            rg.Interval(0, p["eave_height"]),        )        ids.append(_add_brep(box.ToBrep(), layer_louvers,                             f"PAV_louver_{side_label}_{j:02d}"))        counts["Louvers"] += 1_build_louver_set(*louvers_left_y_range, "L", 1)_build_louver_set(*louvers_right_y_range, "R", -1)# ── Reference geometry ─────────────────────────────────────────# Footprint (rectangle)footprint = rg.Rectangle3d(    rg.Plane.WorldXY,    rg.Interval(0, p["length"]),    rg.Interval(0, p["width"]),)ids.append(doc.Objects.AddCurve(footprint.ToNurbsCurve(),                                _attrs(layer_ref, "PAV_ref_footprint")))counts["Reference"] += 1# Portal-frame axes preserve the exact 0..length stations even though end# members are shifted inward to keep physical geometry inside the envelope.for i in range(p["frame_count"]):    fx = i * frame_spacing    axis = rg.LineCurve(rg.Point3d(fx, 0, 0),                        rg.Point3d(fx, p["width"], 0))    ids.append(doc.Objects.AddCurve(        axis, _attrs(layer_ref, f"PAV_ref_frame_axis_{i:02d}")    ))    counts["Reference"] += 1# Entrance opening indicatorent_line = rg.LineCurve(    rg.Point3d(0, ent_start_y, p["eave_height"]),    rg.Point3d(0, ent_end_y, p["eave_height"]),)ids.append(doc.Objects.AddCurve(ent_line,                                _attrs(layer_ref, "PAV_ref_entrance")))counts["Reference"] += 1# ── Validation ─────────────────────────────────────────────────solid_ok = 0solid_bad = 0for oid in ids:    rh_obj = doc.Objects.Find(oid)    if (rh_obj and rh_obj.Attributes.LayerIndex != layer_ref and            hasattr(rh_obj, "Geometry") and rh_obj.Geometry is not None):        geo = rh_obj.Geometry        try:            b = geo if isinstance(geo, rg.Brep) else geo.ToBrep()        except Exception:            solid_bad += 1            continue        if b.IsValid and b.IsSolid:            solid_ok += 1        else:            solid_bad += 1# ── Bounding box (physical objects only) ──────────────────────all_bb = rg.BoundingBox.Unsetfor oid in ids:    obj = doc.Objects.Find(oid)    if obj and obj.Geometry and obj.Attributes.LayerIndex != layer_ref:        bb = obj.Geometry.GetBoundingBox(True)        if bb.IsValid:            all_bb = rg.BoundingBox.Union(all_bb, bb) if all_bb.IsValid else bb# ── Redraw ─────────────────────────────────────────────────────doc.Views.Redraw()# ── Summary ────────────────────────────────────────────────────import jsonsummary = {    "parameters": PARAMS,    "object_counts": counts,    "semantic_counts": semantic_counts,    "total_objects": sum(counts.values()),    "solid_validation": {"physical_ok": solid_ok, "physical_bad": solid_bad},    "frame_axes_x": [i * frame_spacing for i in range(p["frame_count"])],    "frame_spacing": frame_spacing,    "entrance_y_range": [ent_start_y, ent_end_y],    "bounding_box": {        "min": [all_bb.Min.X, all_bb.Min.Y, all_bb.Min.Z],        "max": [all_bb.Max.X, all_bb.Max.Y, all_bb.Max.Z],    },}print(json.dumps(summary, indent=2))