Day 22 – Scripting CAD Environments (Rhino/Grasshopper & Dynamo)


 🏗️ Day 22 – Scripting CAD Environments (Rhino/Grasshopper & Dynamo)

1. Learning Objectives

By the end of Day 22, you will be able to:

  • Understand how Python integrates with Rhino/Grasshopper (via ghPython) and Dynamo for Revit.
  • Write Python scripts to create and manipulate geometry (points, curves, surfaces, Breps) in Rhino.
  • Automate repetitive modelling tasks (generating columns, beams, panels) using loops and parameters.
  • Read external data (CSV, JSON) to drive geometry generation in CAD.
  • Understand the RhinoCommon and Revit API object models.
  • Write a script that generates a parametric truss directly inside Rhino from a Python component.

2. Concept Explanation

2.1 Why Script CAD Environments?

In professional AEC practice, much of the modelling is repetitive – placing hundreds of columns, creating floor plates, generating trusses, or laying out reinforcement. Manually doing this is slow and error‑prone. Python scripting inside CAD tools allows you to:

  • Parametric modelling: Change inputs (span, spacing) and have the model update automatically.
  • Batch processing: Generate entire buildings from a spreadsheet.
  • Interoperability: Read geometry from CSV/Excel and create BIM elements.
  • Custom tools: Build your own commands and components.

2.2 Three Approaches

EnvironmentToolTypical Use Case
RhinoRhino.Python (via py)Direct Rhino commands, batch geometry
GrasshopperghPython componentVisual programming + Python logic
DynamoPython Script nodeRevit automation, parametric families

We'll focus on Rhino Python and Grasshopper ghPython as they are the most common.

2.3 Rhino Python (RhinoCommon)

Rhino's Python editor (EditPythonScript) exposes the full RhinoCommon API. Key namespaces:

  • rhinoscriptsyntax (simplified, script‑like)
  • Rhino.Geometry (points, curves, surfaces, Breps)
  • Rhino.DocObjects (document layers, objects)

Installation: No extra install needed – Rhino includes Python natively.

2.4 Grasshopper ghPython Component

Inside Grasshopper, place a Python component from the Maths > Script tab. It gives you:

  • Inputs and outputs as Grasshopper parameters.
  • Full access to RhinoCommon.
  • Ability to use external libraries (numpy, etc.) if added to Rhino's Python path.

3. Code Examples

Example 1: Rhino Python – Create a column grid from user input

"""
Run this in Rhino's Python editor (Tools > Python > EditPythonScript).
Creates a rectangular column grid as point objects on the current layer.
"""
import rhinoscriptsyntax as rs

# User input
x_count = rs.GetInteger("Number of columns in X direction", 4)
y_count = rs.GetInteger("Number of columns in Y direction", 3)
x_spacing = rs.GetReal("Spacing in X (m)", 6.0)
y_spacing = rs.GetReal("Spacing in Y (m)", 8.0)

# Create points
points = []
for row in range(y_count):
    y = row * y_spacing
    for col in range(x_count):
        x = col * x_spacing
        pt = rs.AddPoint(x, y, 0)
        points.append(pt)

print(f"Created {len(points)} column points.")
rs.ZoomExtents()

Example 2: Grasshopper ghPython – Parametric beam span checker

This component takes a list of spans and a limit, and outputs passes/fails with colour-coded geometry.

"""
Grasshopper ghPython component.
Inputs:
  spans: list[float] – beam spans in metres
  limit: float – maximum allowed span
Outputs:
  results: list[str] – "PASS" or "FAIL"
  geometry: list[Line] – coloured line geometry (red for fail, green for pass)
"""
import Rhino
import Grasshopper

def colour_line(start, end, colour):
    """Create a coloured line for display in Rhino."""
    line = Rhino.Geometry.Line(start, end)
    return line

results = []
geometry = []
for i, span in enumerate(spans):
    if span <= limit:
        results.append("PASS")
        # Green line at y=0
        start = Rhino.Geometry.Point3d(i * 2, 0, 0)
        end = Rhino.Geometry.Point3d(i * 2 + span, 0, 0)
        line = colour_line(start, end, Rhino.Display.ColorRGBA.Green)
        geometry.append(line)
    else:
        results.append("FAIL")
        # Red line at y=1
        start = Rhino.Geometry.Point3d(i * 2, 1, 0)
        end = Rhino.Geometry.Point3d(i * 2 + min(span, limit), 1, 0)
        line = colour_line(start, end, Rhino.Display.ColorRGBA.Red)
        geometry.append(line)

Example 3: Dynamo Python Script – Create Revit columns from CSV

"""
Dynamo Python Script node.
Reads a CSV file with column coordinates and creates structural columns in Revit.
"""
import clr
clr.AddReference('ProtoGeometry')
clr.AddReference('RevitAPI')
clr.AddReference('RevitAPIUI')
clr.AddReference('RevitServices')

from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import StructuralType

import csv
import System

doc = DocumentManager.Instance.CurrentDBDocument

# Inputs from Dynamo
csv_path = IN[0]          # file path
family_name = IN[1]       # e.g., "M_Structural Columns"
level_name = IN[2]        # e.g., "Ground Floor"

# Get the family symbol
collector = FilteredElementCollector(doc).OfClass(FamilySymbol)
family_symbol = None
for fs in collector:
    if fs.Family.Name == family_name and fs.Name == family_name:
        family_symbol = fs
        break

if not family_symbol:
    OUT = "Family symbol not found"
else:
    # Get the level
    levels = FilteredElementCollector(doc).OfClass(Level)
    level = None
    for l in levels:
        if l.Name == level_name:
            level = l
            break
    
    if not level:
        OUT = "Level not found"
    else:
        # Read CSV and place columns
        columns_placed = []
        TransactionManager.Instance.EnsureInTransaction(doc)
        try:
            with open(csv_path, 'r') as f:
                reader = csv.reader(f)
                next(reader)  # skip header
                for row in reader:
                    x = float(row[0])
                    y = float(row[1])
                    point = XYZ(x, y, 0)
                    column = doc.Create.NewFamilyInstance(point, family_symbol, level, StructuralType.Column)
                    columns_placed.append(column)
            TransactionManager.Instance.TransactionTaskDone()
        except Exception as e:
            TransactionManager.Instance.TransactionTaskDone()
            OUT = str(e)
        
        OUT = f"Placed {len(columns_placed)} columns"

Example 4: Rhino Python – Generate a truss from a CSV (from Day 15 project)

"""
Reads a truss CSV (nodes and members) and creates line geometry in Rhino.
CSV format:
  NODE_ID,X_m,Y_m
  B0,0,0
  ...
  
  MEMBER_ID,START_NODE,END_NODE,TYPE,LENGTH_m
  BC1,B0,B1,chord,2.0
"""
import rhinoscriptsyntax as rs
import csv

def import_truss_from_csv(csv_path):
    """Import truss geometry from CSV and draw lines in Rhino."""
    nodes = {}
    members = []
    
    with open(csv_path, 'r') as f:
        reader = csv.reader(f)
        section = None
        for row in reader:
            if not row or len(row) == 0:
                continue
            if row[0] == "NODE_ID":
                section = "nodes"
                continue
            elif row[0] == "MEMBER_ID":
                section = "members"
                continue
            
            if section == "nodes":
                node_id, x, y = row[0], float(row[1]), float(row[2])
                nodes[node_id] = (x, y)
                # Optionally add node point
                rs.AddPoint(x, y, 0)
            elif section == "members":
                mem_id, start_id, end_id, mem_type, length = row[0], row[1], row[2], row[3], float(row[4])
                if start_id in nodes and end_id in nodes:
                    start = nodes[start_id]
                    end = nodes[end_id]
                    line = rs.AddLine((start[0], start[1], 0), (end[0], end[1], 0))
                    # Colour by type
                    colour = (0,0,0)  # default black
                    if mem_type == "chord":
                        colour = (0,0,255)    # blue
                    elif mem_type == "vertical":
                        colour = (0,255,0)    # green
                    elif mem_type == "diagonal":
                        colour = (255,0,0)    # red
                    if line:
                        rs.ObjectColor(line, colour)
                    members.append(mem_id)
    
    print(f"Imported {len(nodes)} nodes and {len(members)} members.")

# Run (replace with your file path)
import_truss_from_csv("truss_12m.csv")
rs.ZoomExtents()

Example 5: Grasshopper ghPython – Parametric staircase generator

"""
Grasshopper ghPython component.
Generates a straight staircase from parameters.
Inputs:
  total_rise: float – total height to climb (m)
  tread_depth: float – depth of each tread (m)
  riser_height: float – height of each riser (m)
  width: float – stair width (m)
Outputs:
  step_lines: list[Line] – tread front edges
  side_lines: list[Line] – stringer outlines
"""
import Rhino
import math

steps = int(math.ceil(total_rise / riser_height))
actual_rise = total_rise / steps

step_lines = []
side_lines = []

for i in range(steps):
    z1 = i * actual_rise
    z2 = (i + 1) * actual_rise
    x1 = i * tread_depth
    x2 = (i + 1) * tread_depth
    
    # Tread front edge
    pt1 = Rhino.Geometry.Point3d(x2, -width/2, z1)
    pt2 = Rhino.Geometry.Point3d(x2, width/2, z1)
    step_lines.append(Rhino.Geometry.Line(pt1, pt2))
    
    # Riser (vertical line at back of tread)
    if i < steps - 1:
        pt3 = Rhino.Geometry.Point3d(x1, -width/2, z1)
        pt4 = Rhino.Geometry.Point3d(x1, -width/2, z2)
        side_lines.append(Rhino.Geometry.Line(pt3, pt4))
    
    # Stringer outline (left side)
    if i == 0:
        pt_left_bottom = Rhino.Geometry.Point3d(0, -width/2, 0)
    pt_left_top = Rhino.Geometry.Point3d(x2, -width/2, z1)
    if i == steps - 1:
        pt_left_top_last = Rhino.Geometry.Point3d(x2, -width/2, actual_rise * steps)
        side_lines.append(Rhino.Geometry.Line(pt_left_top, pt_left_top_last))

4. Hands‑on Exercises (3–5 Problems)

Problem 1 – Rhino Python: Draw a rectangle from user input
Write a script that asks the user for length, width, and a base point. Draw a rectangle (closed polyline) on the ground plane.

Problem 2 – Grasshopper ghPython: Filter beams by span
Create a component that takes a list of beam spans and a limit, and returns two lists: spans that pass and spans that fail. Also output pass/fail counts.

Problem 3 – Rhino Python: Create a layer structure from a list
Given a list floor_levels = ["Ground", "Level 2", "Level 3", "Roof"], create a new layer for each floor and a sub‑layer called "Columns" under each. Use rs.AddLayer().

Problem 4 – Dynamo Python: Place family instances from Excel
Write a Dynamo Python script that reads an Excel file with columns X, Y, Z, Type and places Revit family instances at those coordinates. (Assume the family is already loaded.)

Solutions (attempt first):

# P1 – Rhino Python: Draw rectangle
import rhinoscriptsyntax as rs
base = rs.GetPoint("Pick base point")
if base:
    length = rs.GetReal("Length", 10.0)
    width = rs.GetReal("Width", 6.0)
    if length and width:
        corners = [
            (base.X, base.Y, 0),
            (base.X + length, base.Y, 0),
            (base.X + length, base.Y + width, 0),
            (base.X, base.Y + width, 0),
            (base.X, base.Y, 0)  # close
        ]
        polyline = rs.AddPolyline(corners)
        print(f"Rectangle created: {length}x{width}")

# P2 – Grasshopper ghPython: Filter beams
pass_spans = []
fail_spans = []
for s in spans:
    if s <= limit:
        pass_spans.append(s)
    else:
        fail_spans.append(s)
pass_count = len(pass_spans)
fail_count = len(fail_spans)

# P3 – Rhino Python: Layer structure
floors = ["Ground", "Level 2", "Level 3", "Roof"]
parent_layer = "Building"
if not rs.IsLayer(parent_layer):
    rs.AddLayer(parent_layer)
for floor in floors:
    floor_layer = f"{parent_layer}::{floor}"
    if not rs.IsLayer(floor_layer):
        rs.AddLayer(floor_layer)
    col_layer = f"{floor_layer}::Columns"
    if not rs.IsLayer(col_layer):
        rs.AddLayer(col_layer)
print("Layer structure created.")

# P4 – Dynamo Python: Place families from Excel
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
# ... (similar to Example 3, reading Excel via pandas or xlrd)

5. Applied Challenge Task

Task: Parametric Truss Generator in Rhino/Grasshopper

Build a Grasshopper definition with a Python component that:

  1. Takes inputs:

    • truss_span (float, metres)
    • truss_depth (float, metres)
    • panel_width (float, metres)
    • truss_type (integer: 0 = Pratt, 1 = Warren)
  2. Generates the truss geometry as a list of lines (top chord, bottom chord, verticals, diagonals), each with a colour attribute (blue for chords, green for verticals, red for diagonals).

  3. Adds labels at each node (use Rhino.Geometry.TextDot).

  4. Outputs:

    • nodes: list of points (for visualisation)
    • members: list of lines (geometry)
    • node_labels: list of text dots
    • total_member_length: float (sum of all member lengths)
    • weight_estimate: float (assume 80 kg/m steel)
  5. Bonus: Read parameters from an Excel or CSV file to generate multiple trusses in one run (e.g., a roof of 6 trusses at 4m spacing).

Why this matters:
This task directly applies the parametric truss generator from Day 15 to a real CAD environment. Instead of just writing a CSV, you now create actual 3D geometry that can be used in a BIM model, fabrication drawings, or structural analysis.


6. Brief Review Summary

  • Rhino Python (rhinoscriptsyntax, Rhino.Geometry) allows batch geometry creation and automation.
  • Grasshopper ghPython combines visual programming with Python logic – ideal for parametric components.
  • Dynamo Python uses Revit API via Python scripts to automate BIM elements.
  • Key operations: create points, lines, curves; assign colours; work with layers and documents.
  • Scripting CAD environments turns your Python skills into practical modelling automation.

Key takeaway:
You can now write Python scripts that run inside the most common AEC modelling tools. This means you can automate repetitive tasks, generate complex geometry from parameters, and build custom design tools – all from within the software your team already uses.


7. Preview of Next Topic (Day 23)

Tomorrow is the Phase 3 Weekly Project: BIM Data Analyser CLI.
You’ll build a command‑line tool that:

  • Reads an IFC model.
  • Computes quantities (volumes, areas) by type and by storey.
  • Performs a basic clash check (intersecting bounding boxes).
  • Exports a dashboard HTML report.
  • Uses generators, context managers, and decorators from Days 16–18.

This project consolidates everything from Phase 3 into one integrated BIM analysis tool.

Comments

EARN AT THE COMFORT OF YOUR HOME

Sponsored content

Popular posts from this blog

Day 5 – Functions, Scope, and Docstrings

Day 10 – Exception Handling and Debugging

Day 1 – Python Foundations for AEC Professionals