Chapter 01 · Foundation

What is Scripting?

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."

hexagonal_torus.macroRhino Macro
! Creates a hexagonal torus — hard-coded, no flexibility _SelNone _Polygon _NumSides=6 w0,0,0 w10,0,0 _SelLast -_Properties _Object _Name RailPolygon _Enter _Enter _SelNone _Polygon _NumSides=6 w10,0,0 w12,0,0 _SelLast _Rotate3D w0,0,0 w10,0,0 90 -_Properties _Object _Name ProfilePolygon _Enter _Enter _SelNone -_Sweep1 _SelName RailPolygon _SelName ProfilePolygon _Enter _Enter _Closed=Yes Enter
💡 Localization Tip

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 input somenumber = 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

⚡ 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 rsrs.AddLine().

Chapter 02 · Language

Python Essentials

Python Primer Rev3 — Section 2 (Language origin, Variable data, Flow control)

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 = 12 panel_index = 0 negative_int = -7 # ── DOUBLES / FLOATS — decimal numbers, used for geometry ── wall_height = 3.5 radius = 2.718 scientific = 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 work combined = "Panel" + "_01" # concatenation → "Panel_01" with_number = "Height: " + str(wall_height) # → "Height: 3.5" # ── BOOLEANS — only True or False ────────────────────────── is_visible = True is_locked = False # ── NONE — the "empty" / "failed" value ──────────────────── # All RS methods return None when they fail result = None # Shorthand: assign multiple variables at once x, y, z = [1, 2, 3] # x=1, y=2, z=3 # Check type of anything print(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:

TypePrefixExampleNotes
BooleanblnblnSuccessTrue/False flags
IntegerintintSidesWhole numbers
Double/FloatdbldblRadiusDecimal numbers
StringstrstrLayerNameText values
List/ArrayarrarrPointsCollections
Object IDobjobjCurveRhino GUIDs
ErrorerrerrProjectionExceptions

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 indent if temperature > 30: # LEVEL 1 — 4 spaces print("Hot!") if temperature > 40: # LEVEL 2 — 8 spaces print("Extreme heat!") # Back to LEVEL 0 print("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.

SymbolNameCreates / AccessesPrimary uses
( )ParenthesesTuple / grouped expressionFunction calls, math grouping, tuple literals
[ ]Square BracketsList / indexed accessLists, indexing, slicing, getting/setting items
{ }Curly BracketsDict / SetDictionaries (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 inside print("Hello Rhino") # 1 argument print(3.14, "is approx pi") # 2 arguments rs.AddPoint(0, 0, 0) # 3 args: x, y, z rs.AddLine((0,0,0), (5,5,5)) # 2 args, each a tuple # Type conversion functions int(3.9) # → 3 (truncates decimals) float(5) # → 5.0 str(42) # → "42" bool(0) # → False (0 is falsy) len([1,2,3]) # → 3 # range() is essential for loops range(10) # 0,1,2,...,9 range(2, 8) # 2,3,4,5,6,7 range(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 - 3 C = 2 * x D = 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 tuples rgb_color = (255, 128, 0) # orange t = 12345, 54321, 'hello' # () are optional! # Access by 0-based index point_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 variables x, 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 lists heights = [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 end heights[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 included heights[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.0 heights.append(17.5) # add to end heights.insert(1, 1.75) # insert at position 1 heights.remove(7.0) # remove by value heights.pop() # remove + return last heights.sort() # sort ascending heights.reverse() # reverse in place len(heights) # number of items

3.3 Curly Brackets { } — Dicts and Sets

dicts_sets.pyPython
# Dictionary: key → value pairs material = { 'name' : 'Concrete', 'density' : 2400, 'color' : (200, 200, 195), 'structural': True } material['name'] # → 'Concrete' material.get('cost', 0) # → 0 (safe, no KeyError) material['thickness'] = 0.3 # add new key del material['thickness'] # remove key for key, val in material.items(): print(key, '→', val) # Set: unique values only layers = {"Wall", "Floor", "Wall", "Roof"} print(layers) # → {'Wall', 'Floor', 'Roof'} — duplicate removed # Practical: deduplicate a list unique = list(set(["A", "B", "A", "C", "B"]))
Chapter 04 · Operations

Operators & Functions

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

CategoryOperatorMeaningExample
Arithmetic=Assign valuex = 5
+ - * /Add, subtract, multiply, dividex = (a+b) * c / d
//Floor division (integer result)10 // 3 → 3
**Power / exponentx = x ** 2.3
%Modulo (remainder)10 % 3 → 1
+= -= *=Augmented assignmentx += 1 ≡ x = x+1
+ (str)String concatenation"Panel" + "_A"
str()Number → string conversionstr(3.14) → "3.14"
Comparison< <= > >=Less/greater (or equal)if x < 5:
==Equal to (not assignment!)if x == 10:
!=Not equal toif x != 0:
isSame object identityif x is None:
is notNot same objectif x is not None:
inMember of collectionif "A" in my_list:
Logicaland or notBoolean operatorsif 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 return def MyBogusFunction(intNumber1, intNumber2): intNumber1 = intNumber1 + 100 intNumber2 = intNumber2 * 2 return (intNumber1 > intNumber2) # True or False print(MyBogusFunction(5, 6)) # → True (105 > 36) # Optional / default arguments def create_layer(name, color=0, visible=True, locked=False): import rhinoscriptsyntax as rs return rs.AddLayer(name, color, visible, locked) create_layer("Walls") # only name required create_layer("Roof", 255, False) # override some defaults # "Designed not to fail" pattern from the Primer def lockcurves_safe(): import rhinoscriptsyntax as rs curves = rs.ObjectsByType(rs.filter.curve) if not curves: return False # graceful exit rs.LockObjects(curves) return True # Rename an object using system time — real example from Primer import rhinoscriptsyntax as rs import time strObjectID = 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 function def testFunction(): y = 20 return y print(y * testFunction()) # NameError: 'y' is not defined! # CORRECT: y defined globally y = 20 # global scope — readable everywhere def testFunction(): return y # can read global y print(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 if if rs.IsCurve(strObjectID): rs.DeleteObject(strObjectID) # Problem 2: nested if if rs.IsCurve(strObjectID): if rs.CurveLength(strObjectID) < 0.01: rs.DeleteObject(strObjectID) # Problem 3: if / elif / else chain dblLength = rs.CurveLength(strObjectID) if dblLength is None: # 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 True if w > 2.0 and h > 2.0: print("Panel big enough") # or — AT LEAST ONE must be True if w > 10 or h > 10: print("Very large") # not — inverts True↔False if not locked: print("Object is editable") # Combined — use () for clarity if (w > 2 and h > 2) or locked: print("Valid") # Check None — always use 'is' not '==' curve_id = rs.GetObject("Pick a curve") if curve_id is None: return # user cancelled # or shorthand: if not 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 Primer import rhinoscriptsyntax as rs import datetime as dt def viewportclock(): now = dt.datetime.now() textobject_id = rs.AddText(str(now), (0,0,0), 20) if textobject_id is None: return rs.ZoomExtents(None, True) while True: # 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 limit def fitcurvetolength(): curve_id = rs.GetObject("Select curve", rs.filter.curve, True, True) if curve_id is None: return length = rs.CurveLength(curve_id) length_limit = rs.GetReal("Max length", 0.5*length, 0.01*length, length) if length_limit is None: return while True: 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 is None: return print("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 list for z in [0, 3, 6, 9, 12]: print("Floor at:", z) # range(start, stop, step) for i in range(0, 20, 5): # → 0,5,10,15 print(i) # Sine wave — draws points along sin(x) in Rhino for 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 Primer pi = math.pi dblTwistAngle = 0.0 rs.EnableRedraw(False) for z in rs.frange(0.0, 5.0, 0.5): # outer: height dblTwistAngle += pi / 30 for 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 together for i, crv in enumerate(curve_list): print(i, crv) # zip() — iterate two lists in sync for name, h in zip(col_names, col_heights): print(name, "is", h, "m")

6.3 break and continue

break_continue.pyPython
# break — exit loop immediately for i in range(100): if i == 5: break # prints 0,1,2,3,4 then stops print(i) # continue — skip this iteration, go to next for i in range(10): if i % 2 == 0: continue # skip even numbers print(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.

7.1 Tuples — Immutable Sequences

tuples_deep.pyPython
t = 12345, 54321, 'hello!' # tuple without () print(t[0]) # → 12345 print(t) # → (12345, 54321, 'hello!') # Rhino: unpack domain tuple import rhinoscriptsyntax as rs domain = rs.CurveDomain(curve_id) # → (t_min, t_max) t_min, t_max = domain # unpack mid_t = (t_min + t_max) / 2.0 mid_pt = rs.EvaluateCurve(curve_id, mid_t)

7.2 Lists — Mutable Ordered Collections

MethodActionExample
list.append(x)Add to endpts.append(pt)
list.insert(i, x)Insert at index ipts.insert(0, pt)
list.remove(x)Remove first occurrencepts.remove(bad_pt)
list.pop()Remove + return lastlast = pts.pop()
list.pop(i)Remove + return at ifirst = pts.pop(0)
list.sort()Sort in placeheights.sort()
list.reverse()Reverse in placepts.reverse()
list.count(x)Count occurrencesn = vals.count(0)
len(list)Item countn = len(pts)
list[:]Shallow copycopy = pts[:]

7.3 List Comprehensions

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 filter filtered = [3*x for x in myList if x > 3] print(filtered) # → [12, 18] # Rhino: 10x10 point grid in 3 lines import Rhino.Geometry as rg pts = [rg.Point3d(i, j, 0) for i in range(10) for j in range(10)] # 100 points!

7.4 Dictionaries

dicts_deep.pyPython — From Primer Section 6.3
myDict = {'a':1, 'b':2, 'c':3} myDict['d'] = 4 # add myDict['a'] = 10 # update del myDict['b'] # delete print(myDict.keys()) # list of keys # Store surface grid points by (row, col) grid = {} for row in range(5): for col in range(5): grid[(row, col)] = (col, row, 0) # tuple key! # Named neighbor access center = 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, independent x = 10; y = x; x = 5 print(y) # → 10 (unchanged) # Lists — y and x point to THE SAME list object! x = [1, 2] y = x # ← same reference, NOT a copy x.append(3) print(y) # → [1, 2, 3] (y changed too!) # FIX: use [:] to copy x = [1, 2] y = x[:] # ← independent copy x.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
# Basic class class MyClass: """A simple example""" x = 10 def test(self): return 'hello' obj = MyClass() # create an instance obj.x # → 10 obj.test() # → 'hello' obj.x = 5 # modify attribute # __init__ — required data at creation class Panel: def __init__(self, width, height, material="glass"): self.width = width self.height = height self.material = material def area(self): return self.width * self.height def describe(self): return "{} panel {}×{}".format(self.material, self.width, self.height) p1 = Panel(1.2, 2.4) # default material p2 = Panel(0.9, 1.8, "concrete") p1.area() # → 2.88 p2.describe() # → "concrete panel 0.9×1.8" # Inheritance — child class gets all parent methods class GlazedPanel(Panel): # inherits from Panel transparency = 0.8 gp = GlazedPanel(1.0, 2.0) gp.area() # → 2.0 (inherited!) gp.transparency # → 0.8 # isinstance() — check membership isinstance(gp, Panel) # → True isinstance(gp, GlazedPanel) # → True
Chapter 09 · Geometry

Geometry in Rhino

Python Primer Rev3 — Section 8 (complete) · RhinoScript101 — Section 7

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 rs import Rhino.Geometry as rg # RS style — returns GUIDs, modifies Rhino document pt_id = rs.AddPoint(1.0, 2.0, 3.0) # → GUID coords = rs.PointCoordinates(pt_id) # → (x, y, z) # RG style — in-memory, best for Grasshopper pt = rg.Point3d(1.0, 2.0, 3.0) pt.X; pt.Y; pt.Z # Vector math (rs. style) — from Primer section 6.4 v1 = (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=1 rs.VectorScale(v1, 3.0) # → (3,0,0) rs.VectorReverse(v1) # → (-1,0,0) rs.VectorCreate(pt2, pt1) # → vector from pt1 to pt2 rs.PointAdd(pt1, vec) # move pt1 along vec rs.Distance(pt1, pt2) # distance between 2 points # AddVector helper function — from Primer section 6.4 def AddVector(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 rs import Rhino.Geometry as rg # rs style line_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.To crv = line.ToNurbsCurve() # → NurbsCurve for GH wire # Compute polyline length — from Primer 8.4 def PolylineLength(vertices): total = 0.0 for i in range(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 rs import 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 points ptOrigin = 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 UV srfFrame = rs.SurfaceFrame(surface_id, [u, v]) rs.AddCircle(srfFrame, radius) # circle on surface rs.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
import math, rhinoscriptsyntax as rs # Circle rs.AddCircle(rs.WorldXYPlane(), 5.0) # Ellipse (plane + 2 radii) rs.AddEllipse(rs.WorldXYPlane(), 3.0, 5.0) # Arc: plane + radius + sweep angle rs.AddArc(rs.WorldXYPlane(), 5.0, 90) # Rusin's sphere packing — from Primer 8.6 def DistributeCirclesOnSphere(sphere_r, circle_r): v_count = int((math.pi * sphere_r) / (2 * circle_r)) phi = -0.5 * math.pi phi_step = math.pi / v_count rs.EnableRedraw(False) while phi < 0.5*math.pi: h_count = int((2*math.pi*math.cos(phi)*sphere_r) / (2*circle_r)) if h_count == 0: h_count = 1 theta_step = 2*math.pi / h_count theta = 0 while theta < 2*math.pi - 1e-8: cx = sphere_r*math.cos(theta)*math.cos(phi) cy = sphere_r*math.sin(theta)*math.cos(phi) cz = sphere_r*math.sin(phi) cn = rs.PointSubtract((cx,cy,cz), (0,0,0)) cp = rs.PlaneFromNormal((cx,cy,cz), cn) rs.AddCircle(cp, circle_r) theta += theta_step phi += phi_step rs.EnableRedraw(True)

9.5 NURBS Curves

nurbs_curves.pyPython — Curve smoothing from Primer 8.7
import rhinoscriptsyntax as rs # Create curves pts = [(0,0,0), (1,2,0), (3,1,0), (5,3,0)] rs.AddInterpCurve(pts) # interpolated through points rs.AddCurve(pts, degree=3) # control-point curve # Analyze rs.CurveLength(crv_id) rs.CurveDomain(crv_id) # (t_min, t_max) rs.EvaluateCurve(crv_id, t) # point at parameter t rs.CurveTangent(crv_id, t) # tangent direction at t rs.CurveCurvature(crv_id, t) # full curvature data rs.DivideCurve(crv_id, 100) # 100 equally-spaced points rs.CurvePoints(crv_id) # control point list rs.IsCurveClosed(crv_id) # Curve smoothing algorithm — from Primer 6.6 def smoothcurve(curve_id, s): curve_points = rs.CurvePoints(curve_id) new_curve_points = [] for i in range(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 surfaces rs.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 surfaces uDomain = rs.SurfaceDomain(srf_id, 0) # U direction range vDomain = rs.SurfaceDomain(srf_id, 1) # V direction range pt = rs.EvaluateSurface(srf_id, u, v) # R2 → R3 point nrm = rs.SurfaceNormal(srf_id, [u, v]) # normal at UV uv = rs.SurfaceClosestPoint(srf_id, pt) # R3 → R2 closest # Populate with surface frames — from Primer 8.5 count = 20 uD = rs.SurfaceDomain(srf_id, 0) vD = rs.SurfaceDomain(srf_id, 1) uStep = (uD[1] - uD[0]) / count vStep = (vD[1] - vD[0]) / count rs.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)."

meshes.pyPython
import Rhino.Geometry as rg mesh = rg.Mesh() mesh.Vertices.Add(0, 0, 0) # vertex 0 mesh.Vertices.Add(1, 0, 0) # vertex 1 mesh.Vertices.Add(1, 1, 0) # vertex 2 mesh.Vertices.Add(0, 1, 0) # vertex 3 mesh.Faces.AddFace(0, 1, 2, 3) # quad mesh.Faces.AddFace(0, 1, 2) # triangle mesh.Normals.ComputeNormals() mesh.Compact() # rs. mesh queries import rhinoscriptsyntax as rs rs.MeshVertices(mesh_id) # list of vertex coords rs.MeshFaceVertices(mesh_id) # face → vertex indices rs.MeshArea(mesh_id) rs.MeshVolume(mesh_id) # requires closed mesh
Chapter 10 · Libraries

Imports & Modules

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 prefix import math math.sqrt(16) # → 4.0 # Form 2: import as alias — MOST COMMON in Rhino import rhinoscriptsyntax as rs import Rhino.Geometry as rg import datetime as dt # Form 3: from X import Y — no prefix needed from math import sqrt, pi, sin, cos sqrt(25) # → 5.0 (no "math." prefix) # Form 4: from X import * — avoid, causes name collisions from math import *

10.2 Complete Rhino Import Reference

ImportAliasWhat it providesUse case
rhinoscriptsyntaxrs800+ Rhino methods, works with GUIDsRhino script files, porting from VBScript
Rhino.Geometryrg.NET kernel: Point3d, NurbsCurve, Surface, MeshGHPython, high performance, precise geometry
RhinoRApplication: document, views, commands, RhinoDocAdvanced: doc access, layer trees, custom commands
ghpythonlib.componentsghcompEvery GH component as a Python functionReuse GH nodes: ghcomp.Divide_Curve()
ghpythonlib.treehelpersthDataTree ↔ Python list conversionHandling GH tree structures in code
System.Drawingsd.NET colors, fonts, graphicssd.Color.FromArgb(r,g,b)
System.Collections.Generic.NET typed lists, dictionariesAdvanced .NET interop
mathsqrt, sin, cos, tan, pi, log, exp, floor, ceil, atan2All mathematical operations
randomrandom(), randint(), uniform(), choice(), shuffle()Generative / stochastic design
ospath.join(), listdir(), getcwd(), makedirs()File system operations
os.pathexists(), isdir(), splitext()File path manipulation
timetime(), sleep(), localtime()Timing, pauses, timestamps
datetimedtdatetime.now(), date objectsDate/time scripting, clock example
reRegular expressionsPattern matching in strings
jsonJSON encode/decodeSave/load structured data

10.3 GHPython Starter Template

ghpython_template.pyGHPython Component
#═══════════════════════════════════════════════════════════ # GRASSHOPPER PYTHON COMPONENT TEMPLATE # Right-click inputs/outputs in GH to rename them #═══════════════════════════════════════════════════════════ import rhinoscriptsyntax as rs # document methods import Rhino as R # application level import Rhino.Geometry as rg # in-memory geometry import ghpythonlib.components as ghcomp # GH nodes as functions import ghpythonlib.treehelpers as th # tree ↔ list conversion import System.Drawing as sd # colors, fonts import math import 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.

11.2 Common Errors and Fixes

ErrorCommon CauseFix
IndentationErrorMixed tabs + spaces, wrong indent levelUse 4 spaces only. Enable "Show whitespace" in editor.
NameError: 'x' not definedVariable used before assignment, or wrong scopeCheck where you defined it. Check spelling/case.
TypeError: unsupported operandAdding a string to a numberConvert types: str(n), int(s), float(s)
AttributeErrorMethod doesn't exist on that object typeCheck the API docs — verify method name and object class.
iteration over NoneTypePassing None to a function expecting a listAlways check: if not result: return
Script hangs (no error)while condition never becomes FalseAdd a counter / max iterations, or a break condition.
ZeroDivisionErrorDividing by zeroCheck denominator: if denom != 0: result = n / denom
KeyErrorDict key doesn't existUse dict.get(key, default) instead of dict[key]
IndexError: list index out of rangeAccessing beyond list lengthCheck: if i < len(my_list):

11.3 Defensive Patterns

defensive_coding.pyPython
import rhinoscriptsyntax as rs # Pattern 1: always check user input obj_id = rs.GetObject("Select object") if obj_id is None: return # user pressed Escape # Pattern 2: check every Rhino method return length = rs.CurveLength(obj_id) if length is None: print("Not a curve!"); return # Pattern 3: try/except for expected errors try: result = 10.0 / user_value except ZeroDivisionError: print("Cannot divide by zero") result = 0 # Pattern 4: print debug info print("DEBUG: length =", length) print("DEBUG: type =", type(length)) print("DEBUG: list =", my_list) # Pattern 5: guard against empty collections curves = rs.ObjectsByType(rs.filter.curve) if not 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

⚡ 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.

12.2 Full Examples

gh_point_grid.pyGHPython — Inputs: n_x(int), n_y(int), spacing(float)
import Rhino.Geometry as rg pts = [] for row in range(int(n_y)): for col in range(int(n_x)): pt = rg.Point3d(col * spacing, row * spacing, 0) pts.append(pt) a = pts # list of Point3d → GH Point param
gh_surface_fins.pyGHPython — Inputs: srf(Surface), u_div(int), v_div(int), fin_height(float)
import Rhino.Geometry as rg import math fins = [] uD = srf.Domain(0) vD = srf.Domain(1) for i in range(int(u_div)): for j in range(int(v_div)): u = uD.ParameterAt((i + 0.5) / u_div) v = vD.ParameterAt((j + 0.5) / v_div) pt = srf.PointAt(u, v) nm = srf.NormalAt(u, v) h = math.sin(i * 0.8) * fin_height if h > 0: end_pt = pt + nm * h fins.append(rg.Line(pt, end_pt).ToNurbsCurve()) a = fins # list of NurbsCurve → GH Curve param
gh_classify_color.pyGHPython — Inputs: curve(Curve), threshold(float)
import System.Drawing as sd length = 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 output b = 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 — identical a = 'Hello Rhino' b = "Hello Rhino" # Triple quotes — multiline strings desc = """This script creates a parametric tower with n floors, scaling each level by factor t.""" # Escape characters path = "C:\\Users\\Me\\scripts\\tower.py" # \\ = literal backslash newln = "Line1\nLine2" # \n = newline tab = "Col1\tCol2" # \t = tab quote = "She said \"Yes!\"" # \" = literal quote # Raw strings — ignore escapes (great for paths!) raw = r"C:\Users\Me\scripts\tower.py" # r"..." prefix # String length len("Rhino") # → 5

13.2 String Methods

string_methods.pyPython
s = " Hello Rhino World " # Case s.upper() # " HELLO RHINO WORLD " s.lower() # " hello rhino world " s.title() # " Hello Rhino World " s.capitalize() # " hello rhino world " → only first letter up s.swapcase() # swap all cases # Whitespace s.strip() # "Hello Rhino World" — both ends s.lstrip() # "Hello Rhino World " — left only s.rstrip() # " Hello Rhino World" — right only # Search s.find("Rhino") # → index of first match, or -1 s.index("Rhino") # same but raises ValueError if not found s.count("o") # → how many times "o" appears s.startswith(" H") # → True/False s.endswith(" ") # → True/False "Rhino" in s # → True (membership test) # Replace s.replace("Rhino", "Grasshopper") # new string with substitution s.replace("o", "0", 1) # replace only first occurrence # Split and Join words = "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 char s[-1] # "s" — last char s[0:5] # "Rhino" — indices 0,1,2,3,4 s[5:] # "ceros" — from index 5 to end s[:5] # "Rhino" — from start to index 4 s[::] # "Rhinoceros" — full copy s[::-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"

13.4 f-strings and Formatting (Modern Python)

fstrings.pyPython
# f-strings (Python 3.6+) — preferred modern approach name = "Column_A1" length = 3.14159 count = 42 msg = f"Object: {name}, Length: {length:.2f} mm" # → "Object: Column_A1, Length: 3.14 mm" # Format specifiers inside { }: f"{length:.4f}" # "3.1416" — 4 decimal places f"{length:8.2f}" # " 3.14" — width 8, right-aligned f"{length:e}" # "3.141590e+00" — scientific notation f"{count:04d}" # "0042" — zero-padded integer f"{count:b}" # "101010" — binary representation f"{count:,}" # "42" — thousands comma separator f"{name!r}" # "'Column_A1'" — repr, adds quotes f"{name!u}" # "COLUMN_A1" — uppercase conversion (!u not standard; use .upper()) # Expressions inside f-strings f"{2 ** 10}" # "1024" f"Area = {length * length:.2f}" # "Area = 9.87" f"{'yes' if count > 10 else 'no'}" # conditional inside f-string # .format() — older but still widely used "Object: {}, Length: {:.2f}".format(name, length) "Item {0}: {1}".format(1, "beam") # positional "Name: {n}, Val: {v}".format(n="x", v=3) # keyword # Rhino scripting example import rhinoscriptsyntax as rs curves = rs.ObjectsByType(rs.filter.curve) for i, c in enumerate(curves): L = rs.CurveLength(c) rs.ObjectName(c, f"crv_{i:03d}_L{L:.0f}") # "crv_007_L142"
Chapter 14 · Error Handling

Error Handling — try / except / finally

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 structure try: result = 10 / user_value # risky code except ZeroDivisionError: result = 0 # handle specific error print("Cannot divide by zero — defaulting to 0") # Multiple except clauses try: val = int(rs.GetString("Enter a number")) result = 100 / val except ValueError: print("That wasn't a number!") except ZeroDivisionError: print("Cannot divide by zero!") except Exception as 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 what except: pass

14.2 Common Exception Types

ExceptionWhen It OccursExample
ValueErrorWrong type/value for an operationint("abc")
TypeErrorWrong type for an operator"3" + 3
ZeroDivisionErrorDivision by zero10 / 0
IndexErrorList index out of rangelst[99] when len=3
KeyErrorDict key doesn't existd["missing"]
AttributeErrorObject has no such attributeNone.append(1)
NameErrorVariable not definedprint(x) before x exists
FileNotFoundErrorFile doesn't existopen("missing.txt")
ImportErrorModule not foundimport unknown
RuntimeErrorGeneric runtime errorRhino geometry failures

14.3 Raising Exceptions

raising_exceptions.pyPython
# Raise a built-in exception with custom message def set_scale(factor): if factor <= 0: raise ValueError(f"Scale factor must be positive, got {factor}") return factor # Custom exception class class InvalidGeometryError(Exception): def __init__(self, msg="Invalid geometry"): super().__init__(msg) def validate_curve(crv_id): if crv_id is None: raise InvalidGeometryError("No curve selected") length = rs.CurveLength(crv_id) if length is None: raise InvalidGeometryError("Object is not a curve") return length # Use it try: 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 rs import math def solve_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 / 180 if angle_data else 0.0 try: z = eval(function) # evaluate string like "math.cos(D)" except: z = 0 # if formula is invalid, default to 0 return z # Build a parametric mesh surface from f(x,y) formula = "math.cos(math.sqrt(x**2+y**2))" res = 20 pts = [] for i in range(res): for j in range(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) ───────── with open("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 ─────────────────────────────────────────── with open("tower_settings.txt", "r") as f: content = f.read() # entire file as one string with open("tower_settings.txt", "r") as f: lines = f.readlines() # list of lines (incl \n) with open("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
def save_settings(func, domain, resolution): """Save mesh function settings to file.""" with open("MeshSettings_XY.txt", "w") as f: f.write(func + "\n") for d in domain: f.write(str(d) + "\n") f.write(str(resolution) + "\n") def load_settings(): """Load settings — returns defaults if file missing.""" try: with open("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 = 50 return func, domain, resolution

15.3 JSON — Structured Data (Recommended)

json_io.pyPython
import json import rhinoscriptsyntax as rs # JSON is ideal for nested/structured data data = { "project": "Tower_A", "floors": 12, "points": [[0,0,0], [5,0,0], [5,5,0]], "materials": {"beam": "steel", "slab": "concrete"} } # Save to JSON file with open("tower_data.json", "w") as f: json.dump(data, f, indent=2) # indent=2 for readability # Load from JSON file with open("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 string back = json.loads(json_str) # JSON string → data

15.4 Working with Paths (os module)

os_paths.pyPython
import os path = r"C:\Users\Me\Projects\tower.txt" os.path.exists(path) # True/False — path exists? os.path.isfile(path) # True if it's a file os.path.isdir(path) # True if it's a folder os.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 directory script_dir = os.path.dirname(__file__) # current script location data_path = os.path.join(script_dir, "data.json") # List files in directory files = 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: expression square = lambda x: x ** 2 square(5) # → 25 add = lambda a, b: a + b add(3, 4) # → 7 # From Primer §7.1 — midpoint function between = lambda a, b: (a + b) / 2.0 between(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 curve longest = max(curves, key=lambda c: rs.CurveLength(c)) # Sort points by Z height pts_sorted = sorted(points, key=lambda p: p[2]) # by Z pts_sorted_r = sorted(points, key=lambda p: p[2], reverse=True) # filter() — keep only items matching condition long_curves = list(filter(lambda c: rs.CurveLength(c) > 5, curves)) # map() — transform every item lengths = 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 arguments def add_points(*points): """Accept any number of points.""" for pt in points: rs.AddPoint(pt) add_points([0,0,0]) # one point add_points([0,0,0], [5,0,0], [5,5,0]) # three points # **kwargs — variable number of keyword arguments def tag_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 types def full_signature(required, default=10, *extra, **options): print(required, default, extra, options) # Unpack list/dict into function call coords = [1, 2, 3] rs.AddPoint(*coords) # same as rs.AddPoint(1, 2, 3) — unpack list params = {"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!) def grid_points(rows, cols, spacing): """Yields 3D grid points one by one.""" for r in range(rows): for c in range(cols): yield [c * spacing, r * spacing, 0] # Use it — processes one point at a time for pt in grid_points(10, 10, 2.0): rs.AddPoint(pt) # perfect — 100 points, never builds big list # Convert generator to list if you need all at once all_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 generators sum(x ** 2 for x in range(10)) # 285 min(rs.CurveLength(c) for c in curves) # shortest curve any(rs.IsCurveClosed(c) for c in curves) # True if any closed all(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 grid grid = [[i * j for j in range(5)] for i in range(5)] # grid[r][c] = r*c (multiplication table) # Flatten a 2D list flat = [pt for row in grid_2d for pt in row] # Conditional comprehension even = [x for x in range(20) if x % 2 == 0] # Conditional value (ternary) colors = [(255,0,0) if L > 5 else (0,0,255) for L in lengths] # Dict comprehension name_to_len = {rs.ObjectName(c): rs.CurveLength(c) for c in curves} # Set comprehension — unique values unique_layers = {rs.ObjectLayer(obj) for obj in objects} # Zip + comprehension — pair two lists lines = [rs.AddLine(a, b) for a, b in zip(starts, ends)] # Enumerate + comprehension named = [rs.ObjectName(c, f"beam_{i:03d}") for i, c in enumerate(curves)]
Chapter 17 · RhinoCommon

RhinoCommon — rg. Geometry API

Rhino.Geometry namespace · developer.rhino3d.com/api/RhinoCommon/

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 inRhino scripts, .py filesGrasshopper, advanced scripts
Works onDocument (real Rhino objects)Pure geometry in memory
PerformanceSlower (doc transactions)Faster (no document overhead)
ReturnsGUID stringsGeometry objects
Examplers.AddLine(p1,p2)rg.Line(p1,p2)

17.1 Point3d and Vector3d

rg_point_vector.pyPython · RhinoCommon
import Rhino.Geometry as rg # Point3d p1 = rg.Point3d(0, 0, 0) p2 = rg.Point3d(5, 3, 0) p1.X, p1.Y, p1.Z # access coordinates rg.Point3d.Origin # (0,0,0) # Vector3d v1 = rg.Vector3d(1, 0, 0) # X-axis direction v2 = rg.Vector3d(0, 1, 0) # Y-axis direction rg.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 + Vector3d v_scl = v1 * 5.0 # scale by float cross = rg.Vector3d.CrossProduct(v1, v2) # → ZAxis dot = v1 * v2 # dot product (0.0 = perpendicular) v1.Unitize() # normalize to unit length (in-place) v1.Length # magnitude # Point + Vector → new Point3d p3 = p1 + rg.Vector3d(0, 0, 10) # moved 10 up # Distance dist = p1.DistanceTo(p2)

17.2 Plane, Line, Transform

rg_plane_line.pyPython · RhinoCommon
import Rhino.Geometry as rg # Built-in world planes xy = rg.Plane.WorldXY # Z-up flat plane xz = rg.Plane.WorldXZ # Y-up yz = rg.Plane.WorldYZ # X-up # Create plane from origin + normal origin = rg.Point3d(0, 0, 5) normal = rg.Vector3d(0, 0, 1) plane = rg.Plane(origin, normal) # plane at z=5 # Plane properties plane.Origin # Point3d plane.Normal # Vector3d (Z-axis of plane) plane.XAxis # plane's local X plane.YAxis # plane's local Y # Line line = rg.Line(rg.Point3d(0,0,0), rg.Point3d(10,5,0)) line.Length # float line.Direction # Vector3d (non-unit) line.UnitTangent # Vector3d (unit) line.ClosestPoint(pt, limitToFiniteSegment=True) # Transform — 4×4 matrix operations xf_translate = rg.Transform.Translation(5, 0, 0) xf_rotate = rg.Transform.Rotation(math.pi/4, rg.Vector3d.ZAxis, rg.Point3d.Origin) xf_scale = rg.Transform.Scale(rg.Point3d.Origin, 2.0) xf_combined = xf_translate * xf_rotate # compose transforms pt = rg.Point3d(1, 0, 0) pt.Transform(xf_combined) # apply in-place

17.3 NurbsCurve, Circle, Arc

rg_nurbs.pyPython · RhinoCommon
import Rhino.Geometry as rg import math # Circle (in memory — no document) circle = rg.Circle(rg.Plane.WorldXY, 5.0) # plane, radius circle.Radius # 5.0 circle.Circumference # 2πr circle.PointAt(0) # Point3d at parameter t=0 crv_circle = circle.ToNurbsCurve() # → NurbsCurve object # NurbsCurve from points pts = [rg.Point3d(i, math.sin(i), 0) for i in range(10)] nurbs = rg.NurbsCurve.CreateFromPoints(pts, degree=3) # Query NurbsCurve nurbs.Degree # curve degree nurbs.Points.Count # control point count nurbs.Knots.Count # knot count nurbs.Domain # Interval(t_start, t_end) nurbs.PointAtStart # first point nurbs.PointAtEnd # last point nurbs.GetLength() # float nurbs.PointAt(t) # evaluate at param t nurbs.TangentAt(t) # tangent vector nurbs.CurvatureAt(t) # curvature vector nurbs.FrameAt(t) # moving frame: success, plane # Use in Grasshopper — output directly a = 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 Brep sphere_brep = rg.Brep.CreateFromSphere(rg.Sphere(rg.Point3d.Origin, 5)) box_brep = rg.Brep.CreateFromBox(rg.BoundingBox(0,0,0,10,10,10)) # Boolean union tol = 0.001 # use document tolerance union = rg.Brep.CreateBooleanUnion([sphere_brep, box_brep], tol) # Boolean difference diff = rg.Brep.CreateBooleanDifference([box_brep], [sphere_brep], tol) # Boolean intersection inter = rg.Brep.CreateBooleanIntersection([box_brep], [sphere_brep], tol) # Query Brep box_brep.Faces.Count # 6 for box box_brep.Edges.Count # 12 for box box_brep.Vertices.Count # 8 for box box_brep.IsSolid # True if closed/watertight box_brep.GetBoundingBox(True) # BoundingBox # Closest point on Brep pt = rg.Point3d(20, 0, 0) cp = box_brep.ClosestPoint(pt) # → Point3d on surface # Output to Grasshopper a = union[0] if union else None # 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 rs import math def attractor_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") if not attractor: return rs.EnableRedraw(False) circles = [] max_dist = math.sqrt((rows*spacing)**2 + (cols*spacing)**2) for r in range(rows): for c in range(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) # invert if radius > 0.05: circles.append(rs.AddCircle(center, radius)) rs.EnableRedraw(True) print(f"Created {len(circles)} circles") return circles attractor_circles(15, 15)

18.2 Mesh from Mathematical Function — f(x, y)

mesh_function.pyPython · Primer §8.2 full pattern
import rhinoscriptsyntax as rs import math def mesh_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 in range(res)] ys = [ymin + (ymax-ymin)*j/(res-1) for j in range(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 in range(res-1): for c in range(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 it mesh_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 rs import math def parametric_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 in range(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 in range(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_srf parametric_tower(floors=20, twist_deg=90, segs=6)

18.4 GHPython — Data Trees

datatree.pyGHPython
import ghpythonlib.treehelpers as th import Rhino.Geometry as rg # Build a 2D nested list (rows × cols) rows, cols = 5, 8 grid_pts = [] for r in range(rows): row = [] for c in range(cols): row.append(rg.Point3d(c, r, 0)) grid_pts.append(row) # Convert to DataTree for GH output a = 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) def flatten_tree(tree): flat = [] for branch in tree.Branches: flat.extend(branch) return flat all_pts = flatten_tree(x) # x = DataTree input
💡 The Five Parametric Design Principles
  1. 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.
  2. Disable redraw in loops — always rs.EnableRedraw(False) before bulk creation, True after.
  3. Separate geometry from data — compute positions first (a list of points), then create geometry in one batch operation.
  4. Use generators for large gridsyield points one-by-one instead of building 50k-element lists in memory.
  5. Clean up construction geometry — always rs.DeleteObjects() on temp helpers before returning; give final objects meaningful names and layers.