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
| Environment | Tool | Typical Use Case |
|---|---|---|
| Rhino | Rhino.Python (via py) | Direct Rhino commands, batch geometry |
| Grasshopper | ghPython component | Visual programming + Python logic |
| Dynamo | Python Script node | Revit 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:
Takes inputs:
truss_span(float, metres)truss_depth(float, metres)panel_width(float, metres)truss_type(integer: 0 = Pratt, 1 = Warren)
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).
Adds labels at each node (use
Rhino.Geometry.TextDot).Outputs:
nodes: list of points (for visualisation)members: list of lines (geometry)node_labels: list of text dotstotal_member_length: float (sum of all member lengths)weight_estimate: float (assume 80 kg/m steel)
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
Post a Comment