CH NEO-ZÜRICH EDITION
WEATHER · HAZE 12°C
BLEND OF THE DAY · 07/ROGUE
EST. 2027
THE AEC CYBER MORNING NEWS

PAZ Kaffi

DESIGN · DEMOLITION · CAFFEINE · DISPATCH
EDITION 1020 · 20 October 2026
BROADCAST 04:42 CET
2,400 BROADSHEETS PRINTED
READ TIME · 47 MIN
Pick, Don't Type: One Rhino 8 Python Loop Numbers Your Columns Every 6 m
CODE
FRAME · 07:00
20-10-2026

Pick, Don't Type: One Rhino 8 Python Loop Numbers Your Columns Every 6 m

Part 2 of PAZ's Rhino 8 ScriptEditor series: pick a grid axis, divide it every 6 m with rhinoscriptsyntax and place numbered column markers in any unit.

Part 1 of this series, Ten Minutes in Rhino 8’s ScriptEditor: Your First Python Point, Line and Circle, ended on one trade-off: hard-coded coordinates don’t move when the design moves. So the next skill is to read input from the model instead of typing it in. Its Hack already looped five concentric circles with for i in range(1, 6). Part 2 starts from those two lessons. You draw one grid axis, run place_columns.py, click the axis, and the script divides it every 6 m. At each point it places a flat 0.2 m column marker and a numbered dot (C1, C2 …), puts everything on its own layer, and prints a summary.

The engine is rhinoscriptsyntax, the beginner-friendly Python layer over RhinoCommon. McNeel maintains it in the open at github.com/mcneel/rhinoscriptsyntax, and it grew out of Steve Baer’s RhinoPython work. That matters for this tutorial: when the API reference is silent, you can read the source. We do that twice below.

The Tool: Rhino 8’s ScriptEditor running CPython 3 with rhinoscriptsyntax, built and maintained by Robert McNeel & Associates. It is worth an afternoon because every function is one readable line, the source is public, and the .py file you save today is plain text. Someone opening it in 2070 needs a text editor, not a licence server.

Setup:

# 1. In Rhino 8 (metre template), draw a 30 m line: _Line 0,0,0 30,0,0
# 2. Type ScriptEditor  ->  New  ->  Python 3
# 3. Paste and press F5:
#! python 3
import rhinoscriptsyntax as rs
print(rs.UnitSystemName(abbreviate=True))   # expect: m

First steps:

  1. Pick, and respect Esc. According to the RhinoScriptSyntax API reference, rs.GetObject returns “None: if user did not pick an object”. McNeel’s rhino-8.x source shows that pressing Esc, or Enter without picking, also gives None. So test with if crv is None: before anything else. rs.filter.curve (value 4) means only curves can be clicked. The values live in class filter in McNeel’s selection.py (point = 1, curve = 4, surface = 8, polysurface = 16, textdot = 8192), and they add up: rs.filter.curve | rs.filter.surface (12) accepts both, exactly as the reference’s own example does.
  2. Divide and count. rs.DivideCurveLength(crv, spacing) “divides a curve object into segments of a specified length”, in the reference’s words. It returns a list of points and, with the defaults (create_points=False), adds nothing to the model. Print len(pts). Divisions start at the curve’s start point, so numbering follows the curve direction. If C1 lands at the wrong end, run Dir or Flip on the axis.
  3. Loop with enumerate. for i, pt in enumerate(pts, 1): gives you the number and the point together. That is the step up from part 1’s range().

Now the three gotchas that separate a demo from an office script.

Units. Every rhinoscriptsyntax number is in model units, so 6 m is 6 in a metre template and 6000 in a millimetre one. rs.UnitSystem() returns a code from the reference table: 2 for millimetres (1.0e-3 metres), 3 for centimetres, 4 for metres. The UnitScale page’s own example, rs.UnitScale(3, 4), returns 100.0, metres to centimetres. One line handles the rest: m = rs.UnitScale(rs.UnitSystem(), 4) gives model units per metre (1.0, 100.0 or 1000.0). After that, 6.0 * m works in every template.

The end column. The RhinoCommon docs for Curve.DivideByLength say the segment length applies to “each and every segment (except potentially the last one)” and guarantee only the start point. Whether the end point is returned on an exact multiple is not documented. Users in a 2025 McNeel forum thread reported that Grasshopper’s Divide Length gave 10 points on a 100 mm / 10 line in Rhino 7 and 11 in Rhino 8. The script doesn’t guess. It compares the last point with rs.CurveEndPoint(crv) and appends the end if the gap is larger than rs.UnitAbsoluteTolerance(). The trade-off: on a 32 m axis you get a column at the end and a 2 m stub bay, which is correct for a gridline and wrong for a column rhythm. If you want equal bays instead, use rs.DivideCurve(crv, n).

Flat circles. Pass a Point3d to rs.AddCircle and the circle lands on the active viewport’s construction plane. Run the script with Front active and your columns stand up like coins on edge. (Every office finds this out on a Friday.) Build the plane yourself with rs.PlaneFromNormal(pt, (0, 0, 1)) and the markers always lie flat in plan.

The full script:

#! python 3
import rhinoscriptsyntax as rs

m = rs.UnitScale(rs.UnitSystem(), 4)   # model units per metre
spacing = 6.0 * m
radius = 0.2 * m

crv = rs.GetObject("Pick a grid axis", rs.filter.curve, preselect=True)
if crv is None:
    print("Nothing picked - no columns placed.")
else:
    pts = rs.DivideCurveLength(crv, spacing)
    if not pts:
        print("Axis shorter than the spacing - nothing placed.")
    else:
        end = rs.CurveEndPoint(crv)
        if rs.Distance(pts[-1], end) > rs.UnitAbsoluteTolerance():
            pts.append(end)
        layer = rs.AddLayer("S_Columns", (200, 30, 30))
        rs.EnableRedraw(False)
        new_ids = []
        for i, pt in enumerate(pts, 1):
            plane = rs.PlaneFromNormal(pt, (0, 0, 1))
            new_ids.append(rs.AddCircle(plane, radius))
            new_ids.append(rs.AddTextDot("C{}".format(i), pt))
        rs.ObjectLayer(new_ids, layer)
        rs.EnableRedraw(True)
        print("{} columns, {} bays, spacing {:.0f} {}".format(
            len(pts), len(pts) - 1, spacing,
            rs.UnitSystemName(abbreviate=True)))

Dry run on the 30 m axis: six columns, C1 to C6, five bays, and the Terminal prints 6 columns, 5 bays, spacing 6 m. Switch to a millimetre template, draw the same line as 30000, and the script uses 6000 and 200 without you changing a digit. Run it twice and you still get one layer: in the rhino-8.x source, AddLayer finds the existing name and returns it. rs.ObjectLayer takes the whole list of ids at once, so the user’s current layer is never touched. Text dots stay the same size on screen at any zoom, which is why they suit column tags.

←TODAY: One click on a 30 m axis gives six flat, numbered, layered column markers in any unit template. →3012: Grid logic lives as readable rules next to the model, not as clicks nobody remembers. Fulcrum: Picking geometry instead of typing coordinates is what turns a drawing into a rule.

Atelier: In a small Swiss studio, the structural grid moves in every Vorprojekt and each move costs someone an afternoon of renumbering. A script that reads the axis keeps the numbering in step with the design, and a plain .py file can be reviewed like any other office standard. Monday move: put place_columns.py in a shared office scripts folder and agree on one layer name (S_Columns or your own) that every project uses.

Hack: Label every bay with its real length, so a stub bay from an off-multiple axis shows up before the engineer finds it. Add these lines after the main loop, still inside the else block:

for a, b in zip(pts, pts[1:]):
    mid = (a + b) / 2   # Point3d arithmetic
    rs.AddTextDot("{:.2f} m".format(rs.Distance(a, b) / m), mid)

zip(pts, pts[1:]) pairs each column with its neighbour. Dividing by m turns the distance back into metres, so the label reads 6.00 m in a mm template too. On a 32 m axis the last dot says 2.00 m, and nobody argues with a dot.

PAZ has been working on this kind of thread with offices. As our PAZ-Rhino announcement put it, the starting point is the Rhino task your office repeats every week. Column numbering is a classic. PAZ Academy teaches this path from first script to Grasshopper 2, and it sells Rhino 8 licences too.

Learn-it:

Draw one axis today, run the script, then flip the axis and run it again. Watch C1 change ends.

FILED FROM
CO-SIGNERS
PAZ Academy
CONFIDENCE
HIGH
REPRINTS
© PAZ - PARAMETRIC ACADEMY ZURICH · ALL RIGHTS RESERVED

PAZ Kaffi · multidisciplinary editorial, led by PAZ Academy

⚑ REPORT AN ERROR · SUBMIT A CORRECTION
◂ BACK TO FRONT PAGE · PAZ KAFFI

© 2026 PAZ Academy.