Python Primer Rev3 (Tibbits/2011) · RhinoScript101 (Rutten/McNeel)
Rhinoceros is built on a command-line interface — every mouse click you do can also be expressed as text. Scripting adds intelligence: the ability to make decisions, repeat actions, and respond to geometry dynamically.
1.1 Macros — The Lowest Form
A macro is a prerecorded list of commands for Rhino to execute. From the Primer: "Macros allow you to automate tasks you would normally do by hand but not by brain. Macros cannot be made smart, nor do they react to the things they help create."
Always use underscores before command names (_Line, _Circle) to force English command names. This ensures your macro/script works on all Rhino installations worldwide regardless of the UI language.
1.2 Scripts — Where Intelligence Begins
Scripts are text files interpreted line by line. Unlike macros, they can make decisions, loop, read back results, and talk to the user. Rhino supports Python (Rhino 5+). The same Python script runs on both Windows and Mac.
first_script.pyPython
import rhinoscriptsyntax as rs# Ask user for a number — dynamic, responds to inputsomenumber = rs.GetReal("Line length")line = rs.AddLine([0,0,0], [somenumber, 0, 0])print("Line inserted with id:", line)
1.3 Running Scripts in Rhino
EditPythonScript — type in Rhino's command line to open the built-in editor. Write and run scripts directly.
RunPythonScript — loads and runs a .py file from disk.
Toolbar button — embed with _RunPythonScript "path/to/script.py".
Grasshopper GHPython — Math → Script → Python 2 (or Python 3) component on the GH canvas.
⚡ Python vs VBScript (RhinoScript)
Rhino historically used VBScript. Python replaces it with: cleaner syntax, Object-Oriented Programming, access to the full .NET framework, cross-platform (Windows + Mac), and a vast ecosystem of community libraries.
All VBScript methods (Rhino.AddLine()) have direct Python equivalents via import rhinoscriptsyntax as rs → rs.AddLine().
Python was released in 1991. It is high-level, readable, and dynamically typed — no variable declarations needed. From the Primer: "Python plays well with others, runs everywhere, is friendly and easy to learn, and is Open!"
2.1 Variables and Data Types
Unlike C++ or Java, Python doesn't require you to declare variable types. Just assign a value and Python figures it out. From the Primer: "You do NOT need to declare variables or variable types! Just simply use them (x=3)!"
all_data_types.pyPython
# ── INTEGERS — whole numbers, used for counting / indexing ──floor_count =12panel_index =0negative_int =-7# ── DOUBLES / FLOATS — decimal numbers, used for geometry ──wall_height =3.5radius =2.718scientific =2.7e40# 2.7 × 10^40 (very large)tiny =-6.2e-12# very small negative# ── STRINGS — text, always in quotes ───────────────────────layer_name ="Facade_Panels"material ='Concrete'# single or double quotes both workcombined ="Panel"+"_01"# concatenation → "Panel_01"with_number ="Height: "+str(wall_height) # → "Height: 3.5"# ── BOOLEANS — only True or False ──────────────────────────is_visible =Trueis_locked =False# ── NONE — the "empty" / "failed" value ────────────────────# All RS methods return None when they failresult =None# Shorthand: assign multiple variables at oncex, y, z = [1, 2, 3] # x=1, y=2, z=3# Check type of anythingprint(type(wall_height)) # → <type 'float'>print(type(layer_name)) # → <type 'str'>
Naming Conventions (Hungarian Notation)
The Rhino Python community uses prefixes to signal variable types at a glance:
Type
Prefix
Example
Notes
Boolean
bln
blnSuccess
True/False flags
Integer
int
intSides
Whole numbers
Double/Float
dbl
dblRadius
Decimal numbers
String
str
strLayerName
Text values
List/Array
arr
arrPoints
Collections
Object ID
obj
objCurve
Rhino GUIDs
Error
err
errProjection
Exceptions
2.2 Indentation — The Most Important Rule
Python uses indentation (spaces) instead of curly braces to define code blocks. This is not style — it is syntax. The standard is exactly 4 spaces per level.
indentation.pyPython
# LEVEL 0 — no indentif temperature >30:# LEVEL 1 — 4 spacesprint("Hot!")if temperature >40:# LEVEL 2 — 8 spacesprint("Extreme heat!")# Back to LEVEL 0print("Done")
⚠️ Never Mix Tabs and Spaces
Python 2 (IronPython / Rhino 5–7) allows mixing but produces unpredictable behavior. Python 3 (Rhino 8) raises a hard TabError. Always use 4 spaces. Rhino's built-in editor inserts spaces automatically when you press Tab.
⚠️ Case Sensitivity Is Critical
Apfelstrudel, apfelstrudel, and APFELSTRUDEL are three completely different variables. This applies to variable names, function names, class names — everything. A mistyped capital letter is one of the most common beginner bugs.
Chapter 03 · Syntax
Brackets: ( ) [ ] { }
Python Primer Rev3 — Sections 2.3, 6 (Tuples, Lists, Dictionaries)
Three bracket types, three completely different purposes. Confusing them is the most common syntax error beginners make. Master these and 80% of syntax errors disappear.
Symbol
Name
Creates / Accesses
Primary uses
( )
Parentheses
Tuple / grouped expression
Function calls, math grouping, tuple literals
[ ]
Square Brackets
List / indexed access
Lists, indexing, slicing, getting/setting items
{ }
Curly Brackets
Dict / Set
Dictionaries (key→value), sets (unique values)
3.1 Parentheses ( ) — Three Uses
USE 1: Calling Functions
function_calls.pyPython + Rhino
import rhinoscriptsyntax as rs# () executes the function; arguments go insideprint("Hello Rhino") # 1 argumentprint(3.14, "is approx pi") # 2 argumentsrs.AddPoint(0, 0, 0) # 3 args: x, y, zrs.AddLine((0,0,0), (5,5,5)) # 2 args, each a tuple# Type conversion functionsint(3.9) # → 3 (truncates decimals)float(5) # → 5.0str(42) # → "42"bool(0) # → False (0 is falsy)len([1,2,3]) # → 3# range() is essential for loopsrange(10) # 0,1,2,...,9range(2, 8) # 2,3,4,5,6,7range(0, 20, 5) # 0, 5, 10, 15 (step=5)
USE 2: Math Grouping — Operator Precedence
From the Primer: "Without extensive use of parenthesis, complex equations would be very nasty indeed. By using parenthesis we can force precedence and easily group different bits of mathematics."
precedence.pyPython
x =4+5*2# → 14 (* first, then +)x = (4+5) *2# → 18 (() forces + first)import math# Break complex equations into named steps:A = x**2+ (x -1)B = x -3C =2* xD = x ** (0.5* x)y = (math.sqrt(A) / B) +abs(C / D)
USE 3: Tuples (Immutable Sequences)
tuples.pyPython
point_3d = (1.0, 2.0, 0.5) # x, y, z — Rhino loves tuplesrgb_color = (255, 128, 0) # oranget =12345, 54321, 'hello'# () are optional!# Access by 0-based indexpoint_3d[0] # → 1.0 (X)point_3d[1] # → 2.0 (Y)point_3d[2] # → 0.5 (Z)point_3d[-1] # → 0.5 (last item)# Unpack into named variablesx, y, z = point_3d# Immutable — this raises TypeError:# point_3d[0] = 5.0 ← ERROR! tuples can't be modified
3.2 Square Brackets [ ] — Lists and Indexing
lists_indexing.pyPython
# Create listsheights = [0, 3.5, 7.0, 10.5, 14.0]names = ["Wall", "Floor", "Roof"]mixed = [42, "text", True, None]empty = []# Indexing: 0-based, negative counts from endheights[0] # → 0 (first)heights[2] # → 7.0 (third)heights[-1] # → 14.0 (last)heights[-2] # → 10.5 (second to last)# Slicing [start:end] — end is NOT includedheights[1:3] # → [3.5, 7.0]heights[:2] # → [0, 3.5] (first 2)heights[3:] # → [10.5, 14.0] (from 3 onward)heights[:] # → full copy of the list# Modify (lists are mutable)heights[0] =-1.0heights.append(17.5) # add to endheights.insert(1, 1.75) # insert at position 1heights.remove(7.0) # remove by valueheights.pop() # remove + return lastheights.sort() # sort ascendingheights.reverse() # reverse in placelen(heights) # number of items
Python Primer Rev3 — Section 4 (Operators, Functions, Mutability)
Operators compute results. Functions package code for reuse. Together they are the machinery of every script.
4.1 Complete Operator Reference
Category
Operator
Meaning
Example
Arithmetic
=
Assign value
x = 5
+ - * /
Add, subtract, multiply, divide
x = (a+b) * c / d
//
Floor division (integer result)
10 // 3 → 3
**
Power / exponent
x = x ** 2.3
%
Modulo (remainder)
10 % 3 → 1
+= -= *=
Augmented assignment
x += 1 ≡ x = x+1
+ (str)
String concatenation
"Panel" + "_A"
str()
Number → string conversion
str(3.14) → "3.14"
Comparison
< <= > >=
Less/greater (or equal)
if x < 5:
==
Equal to (not assignment!)
if x == 10:
!=
Not equal to
if x != 0:
is
Same object identity
if x is None:
is not
Not same object
if x is not None:
in
Member of collection
if "A" in my_list:
Logical
and or not
Boolean operators
if A and B:
⚠️ Python 2 Integer Division (Rhino 5–7)
In Python 2: 10 / 3 = 3 (not 3.333). To force float division: use float(10) / 3 or 10.0 / 3. Python 3 (Rhino 8) behaves correctly: 10 / 3 = 3.333.
4.2 Writing Functions with def
functions_complete.pyPython — From Python Primer Section 4.4
# Basic function with returndefMyBogusFunction(intNumber1, intNumber2): intNumber1 = intNumber1 +100 intNumber2 = intNumber2 *2return (intNumber1 > intNumber2) # True or Falseprint(MyBogusFunction(5, 6)) # → True (105 > 36)# Optional / default argumentsdefcreate_layer(name, color=0, visible=True, locked=False):import rhinoscriptsyntax as rsreturn rs.AddLayer(name, color, visible, locked)create_layer("Walls") # only name requiredcreate_layer("Roof", 255, False) # override some defaults# "Designed not to fail" pattern from the Primerdeflockcurves_safe():import rhinoscriptsyntax as rs curves = rs.ObjectsByType(rs.filter.curve)ifnot curves: returnFalse# graceful exit rs.LockObjects(curves)returnTrue# Rename an object using system time — real example from Primerimport rhinoscriptsyntax as rsimport timestrObjectID = rs.GetObject("Select an object to rename", 0, False, True)if strObjectID: strNewName ="Time: "+str(time.localtime()) rs.ObjectName(strObjectID, strNewName)
⚡ print() vs return — Critical Difference
print() writes to the console/command-line for debugging. It does NOT pass the value anywhere.
return sends a value OUT of the function to the calling code. Always use return for outputs; use print() only to see what's happening during development.
4.3 Variable Scope
Variables defined inside a function only exist inside that function. They cannot be read outside unless you return them.
scope.pyPython — From Python Primer Section 4.4.2
# WRONG: y is local to the functiondeftestFunction(): y =20return yprint(y *testFunction()) # NameError: 'y' is not defined!# CORRECT: y defined globallyy =20# global scope — readable everywheredeftestFunction():return y # can read global yprint(testFunction()) # → 20
Chapter 05 · Decisions
Conditional Execution
Python Primer Rev3 — Section 5.1 (What if?)
From the Primer: "A major part of programming is recovering from screw-ups. A piece of code does not always behave in a straightforward manner and we need to catch these aberrations before they propagate too far."
5.1 if / elif / else
conditionals.pyPython + Rhino — From Primer Section 5.1
import rhinoscriptsyntax as rs# Problem 1: simple ifif rs.IsCurve(strObjectID): rs.DeleteObject(strObjectID)# Problem 2: nested ifif rs.IsCurve(strObjectID):if rs.CurveLength(strObjectID) <0.01: rs.DeleteObject(strObjectID)# Problem 3: if / elif / else chaindblLength = rs.CurveLength(strObjectID)if dblLength isNone: # check None first! category ="Invalid"elif dblLength < rs.UnitAbsoluteTolerance(): # shorter than tolerance rs.DeleteObject(strObjectID) category ="Deleted"elif dblLength <10* rs.UnitAbsoluteTolerance(): rs.SelectObject(strObjectID) # select for review category ="Ambiguous"else: rs.UnselectObject(strObjectID) category ="Long"
🔬 How the Chain Works
Python checks conditions top-to-bottom. The moment one is True, its block runs and ALL other elif/else blocks are skipped. Only one block ever executes per run.
5.2 Boolean Logic — Venn Diagrams
From the Primer: "A good way to exercise your own boolean logic is to use Venn-diagrams. A Venn diagram is a graphical representation of boolean sets, where every region contains a (sub)set of values that share a common property."
boolean_logic.pyPython
w =5.0; h =3.0; locked =False# and — BOTH must be Trueif w >2.0and h >2.0:print("Panel big enough")# or — AT LEAST ONE must be Trueif w >10or h >10:print("Very large")# not — inverts True↔Falseifnot locked:print("Object is editable")# Combined — use () for clarityif (w >2and h >2) or locked:print("Valid")# Check None — always use 'is' not '=='curve_id = rs.GetObject("Pick a curve")if curve_id isNone: return# user cancelled# or shorthand:ifnot curve_id: return# True if None, 0, or []
Chapter 06 · Repetition
Loops — while and for
Python Primer Rev3 — Sections 5.3, 5.4 (Looping)
Loops are the single most powerful tool in computational design — they turn a 3-line script into one that generates thousands of objects. Master them.
6.1 while Loops — Until a Condition Is Met
while_loops.pyPython — From Primer (Curve scaling example)
# Viewport clock — infinite while loop, real example from Primerimport rhinoscriptsyntax as rsimport datetime as dtdefviewportclock(): now = dt.datetime.now() textobject_id = rs.AddText(str(now), (0,0,0), 20)if textobject_id isNone: return rs.ZoomExtents(None, True)whileTrue: # loop forever rs.Sleep(1000) # pause 1 second now = dt.datetime.now() rs.TextObjectText(textobject_id, str(now))if __name__ =="__main__":viewportclock()# ─────────────────────────────────────────────────────────# Scale curve until it fits a length limitdeffitcurvetolength(): curve_id = rs.GetObject("Select curve", rs.filter.curve, True, True)if curve_id isNone: return length = rs.CurveLength(curve_id) length_limit = rs.GetReal("Max length", 0.5*length, 0.01*length, length)if length_limit isNone: returnwhileTrue:if rs.CurveLength(curve_id) <= length_limit: break# escape! curve_id = rs.ScaleObject(curve_id, (0,0,0), (0.95,0.95,0.95))if curve_id isNone: returnprint("New length:", rs.CurveLength(curve_id))
6.2 for Loops — Known Collections
for_loops.pyPython — Sine wave + sphere distribution from Primer
import math, rhinoscriptsyntax as rs# Basic for over a listfor z in [0, 3, 6, 9, 12]:print("Floor at:", z)# range(start, stop, step)for i inrange(0, 20, 5): # → 0,5,10,15print(i)# Sine wave — draws points along sin(x) in Rhinofor x in rs.frange(-8.0, 8.0, 0.25): y =2* math.sin(x) rs.AddPoint([x, y, 0])# Nested loops: helical sphere distribution — from Primerpi = math.pidblTwistAngle =0.0rs.EnableRedraw(False)for z in rs.frange(0.0, 5.0, 0.5): # outer: height dblTwistAngle += pi /30for a in rs.frange(0.0, 2*pi, pi/15): # inner: angle x =5* math.sin(a + dblTwistAngle) y =5* math.cos(a + dblTwistAngle) rs.AddSphere([x, y, z], 0.5)rs.EnableRedraw(True)# enumerate() — index + value togetherfor i, crv inenumerate(curve_list):print(i, crv)# zip() — iterate two lists in syncfor name, h inzip(col_names, col_heights):print(name, "is", h, "m")
6.3 break and continue
break_continue.pyPython
# break — exit loop immediatelyfor i inrange(100):if i ==5: break# prints 0,1,2,3,4 then stopsprint(i)# continue — skip this iteration, go to nextfor i inrange(10):if i %2==0: continue# skip even numbersprint(i) # prints 1,3,5,7,9
Chapter 07 · Data
Tuples, Lists & Dictionaries
Python Primer Rev3 — Section 6 (complete chapter)
From the Primer: "Tuples, Lists and Dictionaries are just a collection of things! That's really all there is to it." — but the details matter enormously.
From the Primer: "List comprehensions are a way of utilizing the functionality of Lists and For-Loops with very concise syntax."
list_comprehensions.pyPython
myList = [2, 4, 6]tripled = [3*x for x in myList]print(tripled) # → [6, 12, 18]# With condition filterfiltered = [3*x for x in myList if x >3]print(filtered) # → [12, 18]# Rhino: 10x10 point grid in 3 linesimport Rhino.Geometry as rgpts = [rg.Point3d(i, j, 0)for i inrange(10)for j inrange(10)] # 100 points!
7.4 Dictionaries
dicts_deep.pyPython — From Primer Section 6.3
myDict = {'a':1, 'b':2, 'c':3}myDict['d'] =4# addmyDict['a'] =10# updatedel myDict['b'] # deleteprint(myDict.keys()) # list of keys# Store surface grid points by (row, col)grid = {}for row inrange(5):for col inrange(5): grid[(row, col)] = (col, row, 0) # tuple key!# Named neighbor accesscenter = grid[(2, 2)]neighbor_up = grid.get((1, 2)) # safe — None if missing
7.5 Mutability — The Source of Bugs
mutability.pyPython — From Primer Section 4.5
# Numbers — y gets a VALUE copy, independentx =10; y = x; x =5print(y) # → 10 (unchanged)# Lists — y and x point to THE SAME list object!x = [1, 2]y = x # ← same reference, NOT a copyx.append(3)print(y) # → [1, 2, 3] (y changed too!)# FIX: use [:] to copyx = [1, 2]y = x[:] # ← independent copyx.append(3)print(y) # → [1, 2] (unchanged)# Dicts — use .copy()x = {1:'a', 2:'b'}y = x.copy() # ← independent copy
Chapter 08 · OOP
Classes
Python Primer Rev3 — Section 7 (Classes)
From the Primer: "Classes give us another level of functionality and actually define a specific type of programming called Object-Oriented Programming." — Point3d, NurbsCurve, Surface are all classes you use constantly.
classes_complete.pyPython — From Primer Section 7.1
From the Primer: "All objects in Rhino are composed of a geometry part and an attribute part. The attributes store information such as object name, color, layer. Not all attributes make sense for all geometry types."
9.1 Points and Vectors
From the Primer: "There is a practical difference though; points are absolute, vectors are relative. When we treat a list of three doubles as a point it represents a certain coordinate in space, when we treat it as a vector it represents a certain direction."
points_vectors.pyPython
import rhinoscriptsyntax as rsimport Rhino.Geometry as rg# RS style — returns GUIDs, modifies Rhino documentpt_id = rs.AddPoint(1.0, 2.0, 3.0) # → GUIDcoords = rs.PointCoordinates(pt_id) # → (x, y, z)# RG style — in-memory, best for Grasshopperpt = rg.Point3d(1.0, 2.0, 3.0)pt.X; pt.Y; pt.Z# Vector math (rs. style) — from Primer section 6.4v1 = (1,0,0); v2 = (0,1,0)rs.VectorAdd(v1, v2) # → (1,1,0)rs.VectorSubtract(v1, v2) # → (1,-1,0)rs.VectorCrossProduct(v1, v2) # → (0,0,1) perpendicular!rs.VectorDotProduct(v1, v2) # → 0 (perpendicular = dot 0)rs.VectorLength((3,4,0)) # → 5.0 (Pythagoras)rs.VectorUnitize((0,5,0)) # → (0,1,0) length=1rs.VectorScale(v1, 3.0) # → (3,0,0)rs.VectorReverse(v1) # → (-1,0,0)rs.VectorCreate(pt2, pt1) # → vector from pt1 to pt2rs.PointAdd(pt1, vec) # move pt1 along vecrs.Distance(pt1, pt2) # distance between 2 points# AddVector helper function — from Primer section 6.4defAddVector(vecdir, base_point=[0,0,0]): tip_point = rs.PointAdd(base_point, vecdir) line = rs.AddLine(base_point, tip_point)if line: return rs.CurveArrows(line, 2)
9.2 Lines and Polylines
lines_polylines.pyPython + Rhino
import rhinoscriptsyntax as rsimport Rhino.Geometry as rg# rs styleline_id = rs.AddLine((0,0,0), (5,5,5))pts = [(0,0,0), (1,2,0), (3,1,0), (4,3,0)]poly_id = rs.AddPolyline(pts)# rg style (for GH)line = rg.Line(rg.Point3d(0,0,0), rg.Point3d(5,5,5))line.Length; line.From; line.Tocrv = line.ToNurbsCurve() # → NurbsCurve for GH wire# Compute polyline length — from Primer 8.4defPolylineLength(vertices): total =0.0for i inrange(0, len(vertices)-1): total += rs.Distance(vertices[i], vertices[i+1])return total
9.3 Planes
From the Primer: "Planes are not genuine objects in Rhino, they are used to define a coordinate system in 3D world space. It's best to think of planes as vectors — they are merely mathematical constructs." A plane = origin point + X axis + Y axis + Normal.
planes.pyPython
import rhinoscriptsyntax as rsimport Rhino.Geometry as rg# World XY plane (most common default)wxy = rg.Plane.WorldXY# Plane from origin + normal (rs.)arrPlane = rs.PlaneFromNormal((5,5,0), (0,0,1))rs.AddPlaneSurface(arrPlane, 5.0, 5.0)# Plane from 3 picked pointsptOrigin = rs.GetPoint("Plane origin")ptX = rs.GetPoint("X axis", ptOrigin)ptY = rs.GetPoint("Y axis", ptOrigin)plane = rs.PlaneFromPoints(ptOrigin, ptX, ptY)# Surface frame — plane tangent to surface at UVsrfFrame = rs.SurfaceFrame(surface_id, [u, v])rs.AddCircle(srfFrame, radius) # circle on surfacers.AddEllipse(srfFrame, r1, r2) # ellipse on surface# Plane rules (from Primer 8.5):# 1. Axis vectors must be unitized (length = 1.0)# 2. All axis vectors must be perpendicular to each other# 3. X and Y axes are ordered anti-clockwise
9.4 Circles, Ellipses and Arcs
From the Primer on Rusin's algorithm for packing circles on a sphere: "Solve how many circles you can evenly stack from north to south pole. For each band, solve how many circles fit around the sphere. Do it."
circles_arcs.pyPython — Rusin sphere packing from Primer 8.6
nurbs_curves.pyPython — Curve smoothing from Primer 8.7
import rhinoscriptsyntax as rs# Create curvespts = [(0,0,0), (1,2,0), (3,1,0), (5,3,0)]rs.AddInterpCurve(pts) # interpolated through pointsrs.AddCurve(pts, degree=3) # control-point curve# Analyzers.CurveLength(crv_id)rs.CurveDomain(crv_id) # (t_min, t_max)rs.EvaluateCurve(crv_id, t) # point at parameter trs.CurveTangent(crv_id, t) # tangent direction at trs.CurveCurvature(crv_id, t) # full curvature datars.DivideCurve(crv_id, 100) # 100 equally-spaced pointsrs.CurvePoints(crv_id) # control point listrs.IsCurveClosed(crv_id)# Curve smoothing algorithm — from Primer 6.6defsmoothcurve(curve_id, s): curve_points = rs.CurvePoints(curve_id) new_curve_points = []for i inrange(1, len(curve_points)-1): pm = (curve_points[i-1] + curve_points[i+1]) /2.0 va = rs.VectorCreate(pm, curve_points[i]) vm = rs.VectorScale(va, s) new_curve_points.append(rs.PointAdd(curve_points[i], vm)) knots = rs.CurveKnots(curve_id) degree = rs.CurveDegree(curve_id) weights = rs.CurveWeights(curve_id) new_id = rs.AddNurbsCurve(new_curve_points, knots, degree, weights)if new_id: rs.DeleteObject(curve_id)return new_id
9.6 Surfaces
surfaces.pyPython — Surface framing from Primer 8.9
import rhinoscriptsyntax as rs# Create surfacesrs.AddPlanarSrf(closed_curves)rs.AddLoftSrf(profile_curves)rs.AddRevSrf(curve, axis_start, axis_end)rs.AddSphere(center, radius)rs.AddCylinder(base_plane, height, radius)# Analyze surfacesuDomain = rs.SurfaceDomain(srf_id, 0) # U direction rangevDomain = rs.SurfaceDomain(srf_id, 1) # V direction rangept = rs.EvaluateSurface(srf_id, u, v) # R2 → R3 pointnrm = rs.SurfaceNormal(srf_id, [u, v]) # normal at UVuv = rs.SurfaceClosestPoint(srf_id, pt) # R3 → R2 closest# Populate with surface frames — from Primer 8.5count =20uD = rs.SurfaceDomain(srf_id, 0)vD = rs.SurfaceDomain(srf_id, 1)uStep = (uD[1] - uD[0]) / countvStep = (vD[1] - vD[0]) / countrs.EnableRedraw(False)for u in rs.frange(uD[0], uD[1], uStep):for v in rs.frange(vD[0], vD[1], vStep): frame = rs.SurfaceFrame(srf_id, [u, v]) rs.AddPlaneSurface(frame, 1.0, 1.0)rs.EnableRedraw(True)
9.7 Meshes
From the Primer: "Meshes are composed of vertices and faces. Two key concepts: Geometry (where vertices are in space) and Topology (which vertices are connected by which faces)."
Python Primer Rev3 — Section 3.3 + community reference
Everything in Python starts with an import. Knowing which library to import — and how — is fundamental to efficient Rhino scripting.
10.1 Four Import Forms
import_forms.pyPython
# Form 1: import module — requires prefiximport mathmath.sqrt(16) # → 4.0# Form 2: import as alias — MOST COMMON in Rhinoimport rhinoscriptsyntax as rsimport Rhino.Geometry as rgimport datetime as dt# Form 3: from X import Y — no prefix neededfrom math import sqrt, pi, sin, cossqrt(25) # → 5.0 (no "math." prefix)# Form 4: from X import * — avoid, causes name collisionsfrom math import*
#═══════════════════════════════════════════════════════════# GRASSHOPPER PYTHON COMPONENT TEMPLATE# Right-click inputs/outputs in GH to rename them#═══════════════════════════════════════════════════════════import rhinoscriptsyntax as rs # document methodsimport Rhino as R # application levelimport Rhino.Geometry as rg # in-memory geometryimport ghpythonlib.components as ghcomp # GH nodes as functionsimport ghpythonlib.treehelpers as th # tree ↔ list conversionimport System.Drawing as sd # colors, fontsimport mathimport random# GH INPUTS: x, y (rename in component → right-click)# GH OUTPUTS: a, b (set type hint on each)#── Your code here ──────────────────────────────────────a =None# primary output
Chapter 11 · Debugging
Debugging
Python Primer Rev3 — Section 3.5 (The Debugger)
From the Primer: "The Debugger is an essential tool for any programmer. The first computer bug was found in 1947 when a moth was trapped in Harvard University's Mark II Calculator. The operators removed it and taped it into the log book: 'First actual case of bug being found.'"
11.1 The Rhino Python Debugger
Access it via EditPythonScript in the Rhino command line. The debugger allows you to run your script line by line and inspect variable values at each step.
Breakpoint — click to the left of a line number (red circle appears). Script pauses at that line.
▶ Start — run the script in debug mode.
Step Into — execute current line, entering any function calls.
Step Over — execute current line without going inside functions.
Step Out — run to end of current function, return to caller.
Variable Pane — shows name, value, and type of every variable in scope after each step.
11.2 Common Errors and Fixes
Error
Common Cause
Fix
IndentationError
Mixed tabs + spaces, wrong indent level
Use 4 spaces only. Enable "Show whitespace" in editor.
NameError: 'x' not defined
Variable used before assignment, or wrong scope
Check where you defined it. Check spelling/case.
TypeError: unsupported operand
Adding a string to a number
Convert types: str(n), int(s), float(s)
AttributeError
Method doesn't exist on that object type
Check the API docs — verify method name and object class.
iteration over NoneType
Passing None to a function expecting a list
Always check: if not result: return
Script hangs (no error)
while condition never becomes False
Add a counter / max iterations, or a break condition.
ZeroDivisionError
Dividing by zero
Check denominator: if denom != 0: result = n / denom
KeyError
Dict key doesn't exist
Use dict.get(key, default) instead of dict[key]
IndexError: list index out of range
Accessing beyond list length
Check: if i < len(my_list):
11.3 Defensive Patterns
defensive_coding.pyPython
import rhinoscriptsyntax as rs# Pattern 1: always check user inputobj_id = rs.GetObject("Select object")if obj_id isNone: return# user pressed Escape# Pattern 2: check every Rhino method returnlength = rs.CurveLength(obj_id)if length isNone:print("Not a curve!"); return# Pattern 3: try/except for expected errorstry: result =10.0/ user_valueexcept ZeroDivisionError:print("Cannot divide by zero") result =0# Pattern 4: print debug infoprint("DEBUG: length =", length)print("DEBUG: type =", type(length))print("DEBUG: list =", my_list)# Pattern 5: guard against empty collectionscurves = rs.ObjectsByType(rs.filter.curve)ifnot curves:print("No curves found"); return
Chapter 12 · Grasshopper
Python in Grasshopper
GHPython documentation + community reference
The GHPython component integrates Python directly into the Grasshopper parametric graph. Data flows through wires — no Rhino document manipulation needed. This is the most powerful way to script in Rhino.
12.1 Setup
Add component: Math → Script → Python 2 (or Python 3 in Rhino 8)
Double-click to open the script editor
Right-click inputs/outputs to rename them and set their Type Hint
Default inputs: x, y — default output: a
⚡ rs. vs rg. in Grasshopper
rs. adds objects TO the Rhino document and returns GUIDs. Usually wrong for GH — GH manages its own geometry stream.
rg. creates geometry in memory. These connect to GH Curve, Surface, Point, Mesh parameters directly. Always prefer rg. in GHPython.
import System.Drawing as sdlength = curve.GetLength()if length < threshold *0.5: col = sd.Color.FromArgb(30, 100, 255) # blue = short category ="Short"elif length < threshold: col = sd.Color.FromArgb(50, 220, 100) # green = medium category ="Medium"else: col = sd.Color.FromArgb(255, 80, 30) # red = long category ="Long"a = category # str outputb = col # Color output → GH Custom Preview
💡 GHPython Best Practices
1. Set Type Hints on inputs (right-click) to auto-validate incoming data types.
2. Use List Access (not Item Access) on inputs when you want all connected items at once.
3. print() output appears in the GH component's output bubble — perfect for quick debugging.
4. Wrap heavy operations with rs.EnableRedraw(False) / rs.EnableRedraw(True) for big speed gains.
5. To output a list use a = my_list. For DataTree use ghpythonlib.treehelpers.
Chapter 13 · Strings
Strings — The Complete Guide
Python Primer Rev3 — Ch 2.3 + Python Docs
Strings are sequences of characters. In Rhino scripting you use them constantly — for object names, layer names, user prompts, file paths, and output messages. Python strings are immutable: every operation returns a new string rather than modifying in place.
13.1 Creating Strings
strings_create.pyPython
# Single or double quotes — identicala ='Hello Rhino'b ="Hello Rhino"# Triple quotes — multiline stringsdesc ="""This script creates aparametric tower with n floors,scaling each level by factor t."""# Escape characterspath ="C:\\Users\\Me\\scripts\\tower.py"# \\ = literal backslashnewln ="Line1\nLine2"# \n = newlinetab ="Col1\tCol2"# \t = tabquote ="She said \"Yes!\""# \" = literal quote# Raw strings — ignore escapes (great for paths!)raw =r"C:\Users\Me\scripts\tower.py"# r"..." prefix# String lengthlen("Rhino") # → 5
13.2 String Methods
string_methods.pyPython
s =" Hello Rhino World "# Cases.upper() # " HELLO RHINO WORLD "s.lower() # " hello rhino world "s.title() # " Hello Rhino World "s.capitalize() # " hello rhino world " → only first letter ups.swapcase() # swap all cases# Whitespaces.strip() # "Hello Rhino World" — both endss.lstrip() # "Hello Rhino World " — left onlys.rstrip() # " Hello Rhino World" — right only# Searchs.find("Rhino") # → index of first match, or -1s.index("Rhino") # same but raises ValueError if not founds.count("o") # → how many times "o" appearss.startswith(" H") # → True/Falses.endswith(" ") # → True/False"Rhino"in s # → True (membership test)# Replaces.replace("Rhino", "Grasshopper") # new string with substitutions.replace("o", "0", 1) # replace only first occurrence# Split and Joinwords ="beam,column,slab".split(",") # ["beam","column","slab"]lines ="a\nb\nc".splitlines() # ["a","b","c"]joined =" | ".join(words) # "beam | column | slab"path ="/".join(["home","user","file"]) # "home/user/file"# Padding and alignment"42".zfill(5) # "00042" — zero-pad to width 5"Hi".ljust(10, ".") # "Hi........""Hi".rjust(10, ".") # "........Hi""Hi".center(10, "-") # "----Hi----"# Check content"123".isdigit() # True — all digits"abc".isalpha() # True — all letters"abc3".isalnum() # True — letters or digits" ".isspace() # True — all whitespace
13.3 String Indexing and Slicing
string_slicing.pyPython
s ="Rhinoceros"# R h i n o c e r o s# 0 1 2 3 4 5 6 7 8 9 (forward)# -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 (backward)s[0] # "R" — first chars[-1] # "s" — last chars[0:5] # "Rhino" — indices 0,1,2,3,4s[5:] # "ceros" — from index 5 to ends[:5] # "Rhino" — from start to index 4s[::] # "Rhinoceros" — full copys[::-1] # "soreconiR" — reversed!s[0:10:2] # "Rioer" — every other character# Strings are IMMUTABLE — cannot assign to index:# s[0] = "r" ← TypeError! Use replace() instead:s = s.replace(s[0], "r", 1) # "rhinoceros"
Python Primer Rev3 — Section 8.2 (Exception Handling)
From the Primer: "The try/except statement can be used in Python as a great technique for error handling. First, the user implements a statement to 'Try,' if this works then the statement is executed and we are finished. Otherwise, if an exception occurs we go straight to the 'except' portion."
14.1 try / except / else / finally
try_except.pyPython
# Basic structuretry: result =10/ user_value # risky codeexcept ZeroDivisionError: result =0# handle specific errorprint("Cannot divide by zero — defaulting to 0")# Multiple except clausestry: val =int(rs.GetString("Enter a number")) result =100/ valexcept ValueError:print("That wasn't a number!")except ZeroDivisionError:print("Cannot divide by zero!")exceptExceptionas e:print(f"Unexpected error: {e}") # catch-all# else: runs only when NO exception occurred# finally: ALWAYS runs (cleanup code)try: file =open("data.txt", "r") data = file.read()except FileNotFoundError: data =""print("File not found — using empty data")else:print(f"Loaded {len(data)} characters")finally:try: file.close() # close file no matter whatexcept: pass
14.2 Common Exception Types
Exception
When It Occurs
Example
ValueError
Wrong type/value for an operation
int("abc")
TypeError
Wrong type for an operator
"3" + 3
ZeroDivisionError
Division by zero
10 / 0
IndexError
List index out of range
lst[99] when len=3
KeyError
Dict key doesn't exist
d["missing"]
AttributeError
Object has no such attribute
None.append(1)
NameError
Variable not defined
print(x) before x exists
FileNotFoundError
File doesn't exist
open("missing.txt")
ImportError
Module not found
import unknown
RuntimeError
Generic runtime error
Rhino geometry failures
14.3 Raising Exceptions
raising_exceptions.pyPython
# Raise a built-in exception with custom messagedefset_scale(factor):if factor <=0:raise ValueError(f"Scale factor must be positive, got {factor}")return factor# Custom exception classclassInvalidGeometryError(Exception):def__init__(self, msg="Invalid geometry"):super().__init__(msg)defvalidate_curve(crv_id):if crv_id isNone:raise InvalidGeometryError("No curve selected") length = rs.CurveLength(crv_id)if length isNone:raise InvalidGeometryError("Object is not a curve")return length# Use ittry: crv = rs.GetObject("Curve", rs.filter.curve) L =validate_curve(crv)print(f"Length: {L:.3f}")except InvalidGeometryError as e: rs.MessageBox(str(e))
14.4 Real Rhino Example — Equation Solver (from Primer)
equation_solver.pyPython · Primer §8.2
import rhinoscriptsyntax as rsimport mathdefsolve_equation(function, x, y):"""Safely evaluate a math function at (x,y). Supports variables: x, y, D (distance), A (angle).""" D = math.sqrt(x**2+ y**2) # radial distance angle_data = rs.Angle((0,0,0), (x,y,0)) A = angle_data[0] * math.pi /180if angle_data else0.0try: z =eval(function) # evaluate string like "math.cos(D)"except: z =0# if formula is invalid, default to 0return z# Build a parametric mesh surface from f(x,y)formula ="math.cos(math.sqrt(x**2+y**2))"res =20pts = []for i inrange(res):for j inrange(res): x = (i - res/2) *0.5 y = (j - res/2) *0.5 z =solve_equation(formula, x, y) pts.append([x, y, z]) rs.AddPoint([x, y, z])
Chapter 15 · File I/O
File I/O — Read, Write, JSON
Python Primer Rev3 — Section 8.1 (Data Storage)
From the Primer: "We'll be using a *.txt file to store our data since it involves very little code and it survives a Rhino restart. An *.txt file is a textfile which stores a number of strings in a one-level hierarchical format."
15.1 Reading and Writing Files
file_io.pyPython
# File modes# "r" — read (default). Error if not found.# "w" — write. Creates if missing; OVERWRITES if exists.# "a" — append. Creates if missing; adds to end.# "x" — exclusive create. Error if file already exists.# "rb" / "wb" — binary modes (images, etc.)# ── Write ──────────────────────────────────────────settings = {"rows": 5, "cols": 8, "spacing": 2.0}file =open("tower_settings.txt", "w")file.write(f"rows={settings['rows']}\n")file.write(f"cols={settings['cols']}\n")file.write(f"spacing={settings['spacing']}\n")file.close() # always close!# ── Preferred: with statement (auto-closes) ─────────withopen("tower_settings.txt", "w") as f: f.write(f"rows={settings['rows']}\n") f.write(f"cols={settings['cols']}\n") f.write(f"spacing={settings['spacing']}\n")# file automatically closed here ─────────────────────# ── Read ───────────────────────────────────────────withopen("tower_settings.txt", "r") as f: content = f.read() # entire file as one stringwithopen("tower_settings.txt", "r") as f: lines = f.readlines() # list of lines (incl \n)withopen("tower_settings.txt", "r") as f:for line in f: # iterate line-by-line (memory-efficient) key, val = line.strip().split("=")print(f" {key} → {val}")
15.2 Primer Pattern — Save/Load Script Settings
save_load_settings.pyPython · Primer §8.1 exact pattern
defsave_settings(func, domain, resolution):"""Save mesh function settings to file."""withopen("MeshSettings_XY.txt", "w") as f: f.write(func +"\n")for d in domain: f.write(str(d) +"\n") f.write(str(resolution) +"\n")defload_settings():"""Load settings — returns defaults if file missing."""try:withopen("MeshSettings_XY.txt", "r") as f: items = f.readlines() func = items[0].strip() domain = (float(items[1]), float(items[2]),float(items[3]), float(items[4])) resolution =int(items[5])except:# File missing or corrupt — use defaults func ="math.cos(math.sqrt(x**2+y**2))" domain = (-10.0, 10.0, -10.0, 10.0) resolution =50return func, domain, resolution
15.3 JSON — Structured Data (Recommended)
json_io.pyPython
import jsonimport rhinoscriptsyntax as rs# JSON is ideal for nested/structured datadata = {"project": "Tower_A","floors": 12,"points": [[0,0,0], [5,0,0], [5,5,0]],"materials": {"beam": "steel", "slab": "concrete"}}# Save to JSON filewithopen("tower_data.json", "w") as f: json.dump(data, f, indent=2) # indent=2 for readability# Load from JSON filewithopen("tower_data.json", "r") as f: loaded = json.load(f)print(loaded["project"]) # "Tower_A"for pt in loaded["points"]: rs.AddPoint(pt)# json.dumps / json.loads — string versions (no file)json_str = json.dumps(data) # data → JSON stringback = json.loads(json_str) # JSON string → data
15.4 Working with Paths (os module)
os_paths.pyPython
import ospath =r"C:\Users\Me\Projects\tower.txt"os.path.exists(path) # True/False — path exists?os.path.isfile(path) # True if it's a fileos.path.isdir(path) # True if it's a folderos.path.dirname(path) # "C:\Users\Me\Projects"os.path.basename(path) # "tower.txt"os.path.splitext(path) # ("...\\tower", ".txt")os.path.join("C:\\Users", "Me", "file.txt") # cross-platform join# Get Rhino script directoryscript_dir = os.path.dirname(__file__) # current script locationdata_path = os.path.join(script_dir, "data.json")# List files in directoryfiles = os.listdir(script_dir)txts = [f for f in files if f.endswith(".txt")]
Chapter 16 · Advanced Python
Advanced Python — Lambda, *args, Generators
Python Primer Rev3 — §7.1 (lambda) + Python Docs
These features make your code shorter, more expressive, and more Pythonic. Used extensively in parametric design — especially for sorting, filtering, and functional operations on geometry collections.
16.1 lambda — Inline Functions
lambda.pyPython
# lambda syntax: lambda arguments: expressionsquare =lambda x: x **2square(5) # → 25add =lambda a, b: a + badd(3, 4) # → 7# From Primer §7.1 — midpoint functionbetween =lambda a, b: (a + b) /2.0between(0, 10) # → 5.0# Lambda shines with sorted() / min() / max()curves = rs.ObjectsByType(rs.filter.curve)# Sort curves by length (shortest first)sorted_curves =sorted(curves, key=lambda c: rs.CurveLength(c))# Find the longest curvelongest =max(curves, key=lambda c: rs.CurveLength(c))# Sort points by Z heightpts_sorted =sorted(points, key=lambda p: p[2]) # by Zpts_sorted_r =sorted(points, key=lambda p: p[2], reverse=True)# filter() — keep only items matching conditionlong_curves =list(filter(lambda c: rs.CurveLength(c) >5, curves))# map() — transform every itemlengths =list(map(lambda c: rs.CurveLength(c), curves))# Equivalent with list comprehensions (often preferred)long_curves2 = [c for c in curves if rs.CurveLength(c) >5]lengths2 = [rs.CurveLength(c) for c in curves]
16.2 *args and **kwargs
args_kwargs.pyPython
# *args — variable number of positional argumentsdefadd_points(*points):"""Accept any number of points."""for pt in points: rs.AddPoint(pt)add_points([0,0,0]) # one pointadd_points([0,0,0], [5,0,0], [5,5,0]) # three points# **kwargs — variable number of keyword argumentsdeftag_object(obj_id, **attrs):"""Apply any object attributes by name."""if"name"in attrs: rs.ObjectName(obj_id, attrs["name"])if"layer"in attrs: rs.ObjectLayer(obj_id, attrs["layer"])if"color"in attrs: rs.ObjectColor(obj_id, attrs["color"])tag_object(crv_id, name="Beam_1", layer="Structure", color=(255,0,0))# Combine all parameter typesdeffull_signature(required, default=10, *extra, **options):print(required, default, extra, options)# Unpack list/dict into function callcoords = [1, 2, 3]rs.AddPoint(*coords) # same as rs.AddPoint(1, 2, 3) — unpack listparams = {"name": "A", "layer": "Layer1"}tag_object(crv_id, **params) # unpack dict as keyword args
16.3 Generators and yield
generators.pyPython
# Generator function — uses yield instead of return# Generates values one at a time (memory efficient!)defgrid_points(rows, cols, spacing):"""Yields 3D grid points one by one."""for r inrange(rows):for c inrange(cols):yield [c * spacing, r * spacing, 0]# Use it — processes one point at a timefor pt ingrid_points(10, 10, 2.0): rs.AddPoint(pt) # perfect — 100 points, never builds big list# Convert generator to list if you need all at onceall_pts =list(grid_points(5, 5, 1.0))# Generator expression (like list comp but lazy)lengths_gen = (rs.CurveLength(c) for c in curves) # note: ( ) not [ ]total =sum(lengths_gen) # sum without storing all lengths# Built-in functions that accept generatorssum(x **2for x inrange(10)) # 285min(rs.CurveLength(c) for c in curves) # shortest curveany(rs.IsCurveClosed(c) for c in curves) # True if any closedall(rs.IsCurve(c) for c in curves) # True if all are curves
16.4 Advanced List Comprehensions
list_comprehensions_advanced.pyPython
# Nested list comprehension — 2D gridgrid = [[i * j for j inrange(5)] for i inrange(5)]# grid[r][c] = r*c (multiplication table)# Flatten a 2D listflat = [pt for row in grid_2d for pt in row]# Conditional comprehensioneven = [x for x inrange(20) if x %2==0]# Conditional value (ternary)colors = [(255,0,0) if L >5else (0,0,255) for L in lengths]# Dict comprehensionname_to_len = {rs.ObjectName(c): rs.CurveLength(c) for c in curves}# Set comprehension — unique valuesunique_layers = {rs.ObjectLayer(obj) for obj in objects}# Zip + comprehension — pair two listslines = [rs.AddLine(a, b) for a, b inzip(starts, ends)]# Enumerate + comprehensionnamed = [rs.ObjectName(c, f"beam_{i:03d}") for i, c inenumerate(curves)]
rs. (rhinoscriptsyntax) = high-level, works on the document (adds/modifies actual Rhino objects). rg. (Rhino.Geometry) = low-level geometry kernel — creates geometry objects in memory only. In Grasshopper, you almost always use rg. directly and output via the GH parameter.
rs. vs rg. — When to Use Which
rs. (rhinoscriptsyntax)
rg. (Rhino.Geometry)
Used in
Rhino scripts, .py files
Grasshopper, advanced scripts
Works on
Document (real Rhino objects)
Pure geometry in memory
Performance
Slower (doc transactions)
Faster (no document overhead)
Returns
GUID strings
Geometry objects
Example
rs.AddLine(p1,p2)
rg.Line(p1,p2)
17.1 Point3d and Vector3d
rg_point_vector.pyPython · RhinoCommon
import Rhino.Geometry as rg# Point3dp1 = rg.Point3d(0, 0, 0)p2 = rg.Point3d(5, 3, 0)p1.X, p1.Y, p1.Z # access coordinatesrg.Point3d.Origin# (0,0,0)# Vector3dv1 = rg.Vector3d(1, 0, 0) # X-axis directionv2 = rg.Vector3d(0, 1, 0) # Y-axis directionrg.Vector3d.XAxis# built-in unit X (1,0,0)rg.Vector3d.YAxis# built-in unit Y (0,1,0)rg.Vector3d.ZAxis# built-in unit Z (0,0,1)# Vector math (operator overloading!)v_sum = v1 + v2 # Vector3d + Vector3dv_scl = v1 *5.0# scale by floatcross = rg.Vector3d.CrossProduct(v1, v2) # → ZAxisdot = v1 * v2 # dot product (0.0 = perpendicular)v1.Unitize() # normalize to unit length (in-place)v1.Length # magnitude# Point + Vector → new Point3dp3 = p1 + rg.Vector3d(0, 0, 10) # moved 10 up# Distancedist = p1.DistanceTo(p2)
import Rhino.Geometry as rgimport math# Circle (in memory — no document)circle = rg.Circle(rg.Plane.WorldXY, 5.0) # plane, radiuscircle.Radius # 5.0circle.Circumference # 2πrcircle.PointAt(0) # Point3d at parameter t=0crv_circle = circle.ToNurbsCurve() # → NurbsCurve object# NurbsCurve from pointspts = [rg.Point3d(i, math.sin(i), 0) for i inrange(10)]nurbs = rg.NurbsCurve.CreateFromPoints(pts, degree=3)# Query NurbsCurvenurbs.Degree # curve degreenurbs.Points.Count # control point countnurbs.Knots.Count # knot countnurbs.Domain # Interval(t_start, t_end)nurbs.PointAtStart# first pointnurbs.PointAtEnd# last pointnurbs.GetLength() # floatnurbs.PointAt(t) # evaluate at param tnurbs.TangentAt(t) # tangent vectornurbs.CurvatureAt(t) # curvature vectornurbs.FrameAt(t) # moving frame: success, plane# Use in Grasshopper — output directlya = nurbs # GH can display NurbsCurve objects directly
17.4 Brep and Boolean Operations
rg_brep.pyPython · RhinoCommon
import Rhino.Geometry as rg# Create primitives as Brepsphere_brep = rg.Brep.CreateFromSphere(rg.Sphere(rg.Point3d.Origin, 5))box_brep = rg.Brep.CreateFromBox(rg.BoundingBox(0,0,0,10,10,10))# Boolean uniontol =0.001# use document toleranceunion = rg.Brep.CreateBooleanUnion([sphere_brep, box_brep], tol)# Boolean differencediff = rg.Brep.CreateBooleanDifference([box_brep], [sphere_brep], tol)# Boolean intersectioninter = rg.Brep.CreateBooleanIntersection([box_brep], [sphere_brep], tol)# Query Brepbox_brep.Faces.Count # 6 for boxbox_brep.Edges.Count # 12 for boxbox_brep.Vertices.Count # 8 for boxbox_brep.IsSolid # True if closed/watertightbox_brep.GetBoundingBox(True) # BoundingBox# Closest point on Breppt = rg.Point3d(20, 0, 0)cp = box_brep.ClosestPoint(pt) # → Point3d on surface# Output to Grasshoppera = union[0] if union elseNone# results are lists
Chapter 18 · Parametric Patterns
Parametric Design Patterns
Python Primer Rev3 — Ch 7 & 8 (applied examples)
These are complete, real-world script patterns sourced from the Primer and common computational design practice. Each demonstrates key scripting principles in a concrete architectural/design context.
18.1 Attractor Point Fields
attractor_field.pyPython · Rhino Script
import rhinoscriptsyntax as rsimport mathdefattractor_circles(rows=10, cols=10, spacing=3.0):"""Grid of circles scaled by distance to attractor point. Closer to attractor = larger circle.""" attractor = rs.GetPoint("Pick attractor point")ifnot attractor: return rs.EnableRedraw(False) circles = [] max_dist = math.sqrt((rows*spacing)**2+ (cols*spacing)**2)for r inrange(rows):for c inrange(cols): center = [c * spacing, r * spacing, 0] dist = rs.Distance(center, attractor)# Map distance to radius: near=big, far=small t = dist / max_dist # 0..1 normalized radius = spacing *0.5* (1- t) # invertif radius >0.05: circles.append(rs.AddCircle(center, radius)) rs.EnableRedraw(True)print(f"Created {len(circles)} circles")return circlesattractor_circles(15, 15)
18.2 Mesh from Mathematical Function — f(x, y)
mesh_function.pyPython · Primer §8.2 full pattern
import rhinoscriptsyntax as rsimport mathdefmesh_function_xy(formula, xmin, xmax, ymin, ymax, res):"""Creates a mesh surface from a mathematical function z=f(x,y). Formula examples: "math.sin(x) * math.cos(y)" "math.cos(math.sqrt(x**2 + y**2))" "x**2 / 20 - y**2 / 15" """ xs = [xmin + (xmax-xmin)*i/(res-1) for i inrange(res)] ys = [ymin + (ymax-ymin)*j/(res-1) for j inrange(res)]# Build vertex grid verts = []for y in ys:for x in xs:try: z =eval(formula) # evaluate at (x, y)except: z =0 verts.append([x, y, z])# Build quad face indices faces = []for r inrange(res-1):for c inrange(res-1): i = r * res + c # bottom-left faces.append((i, i+1, i+res+1, i+res)) # quad rs.EnableRedraw(False) mesh_id = rs.AddMesh(verts, faces) rs.EnableRedraw(True) rs.ZoomExtents()return mesh_id# Run itmesh_function_xy("math.cos(math.sqrt(x**2+y**2))", -10,10,-10,10, 40)
18.3 Parametric Tower (Multi-floor)
parametric_tower.pyPython · Rhino Script
import rhinoscriptsyntax as rsimport mathdefparametric_tower(floors=12, floor_height=4.0, base_radius=8.0, top_radius=3.0, twist_deg=45.0, segs=8):"""Twisted parametric tower with variable radius. twist_deg: total rotation from base to top.""" rs.EnableRedraw(False) floor_curves = []for i inrange(floors +1): t = i / floors # 0 → 1 z = i * floor_height # height radius = base_radius + (top_radius - base_radius) * t # lerp angle = math.radians(twist_deg * t) # twist# Build polygon at this floor pts = []for s inrange(segs): a = angle +2* math.pi * s / segs pts.append([radius * math.cos(a), radius * math.sin(a), z]) pts.append(pts[0]) # close polygon floor_curves.append(rs.AddPolyline(pts))# Loft all floor curves into a surface tower_srf = rs.AddLoftSrf(floor_curves) rs.DeleteObjects(floor_curves) # clean up curves rs.EnableRedraw(True) rs.ZoomExtents()return tower_srfparametric_tower(floors=20, twist_deg=90, segs=6)
18.4 GHPython — Data Trees
datatree.pyGHPython
import ghpythonlib.treehelpers as thimport Rhino.Geometry as rg# Build a 2D nested list (rows × cols)rows, cols =5, 8grid_pts = []for r inrange(rows): row = []for c inrange(cols): row.append(rg.Point3d(c, r, 0)) grid_pts.append(row)# Convert to DataTree for GH outputa = th.list_to_tree(grid_pts) # each row = one branch# Flatten a DataTree input to a Python list# (x is a DataTree input from GH)defflatten_tree(tree): flat = []for branch in tree.Branches: flat.extend(branch)return flatall_pts =flatten_tree(x) # x = DataTree input
💡 The Five Parametric Design Principles
Normalize to 0–1 — map all parameters to a 0–1 range (t = i / (n-1)) then remap to desired domain. This separates logic from units.
Disable redraw in loops — always rs.EnableRedraw(False) before bulk creation, True after.
Separate geometry from data — compute positions first (a list of points), then create geometry in one batch operation.
Use generators for large grids — yield points one-by-one instead of building 50k-element lists in memory.
Clean up construction geometry — always rs.DeleteObjects() on temp helpers before returning; give final objects meaningful names and layers.