Simstim

Rhino

Build and tag Rhino geometry from a CSV with Python 3 and RhinoCommon

A 55-line Rhino 8 Python 3 script that reads a CSV, builds layered, named objects with user text, and can be run again safely.

· 5 min read · rhino · python · tutorial · rhinocommon · pipeline

If you come from Houdini, a CSV of positions and attributes is a Table Import SOP and a wrangle. In Blender it is a few lines of bpy. In Rhino 8 it is a Python 3 script that talks to RhinoCommon, the .NET API underneath Rhino. The ideas carry over; the names are different.

We will build one script that reads a CSV of props, makes a sphere for each row, puts it on a layer per category, names it, and stores the asset ID as user text. Running it twice will not duplicate anything.

I ran the finished script in Rhino 8 (8.25.25314.11001) on Windows. The only difference was the CSV path. The output is at the end.

The data

Save this as C:\temp\props.csv:

asset_id,name,category,x,y,z,radius
A001,lamp_post_01,street,0,0,0,0.5
A002,lamp_post_02,street,10,0,0,0.5
A003,bench_01,furniture,5,4,0,1.2
A004,tree_01,foliage,-6,8,0,3.0
A005,tree_02,foliage,14,9,0,2.5

In Houdini terms, each row becomes one piece of geometry. name becomes the object name, category becomes the layer, and asset_id is a string attribute stored on the object.

Step 1: imports and settings

Open the script editor with the ScriptEditor command, make a new Python 3 script and start with this:

#! python3
import csv
import System
import System.Drawing
import Rhino
import scriptcontext as sc

CSV_PATH = r"C:\temp\props.csv"

LAYER_COLORS = {
    "street": System.Drawing.Color.Orange,
    "furniture": System.Drawing.Color.SteelBlue,
    "foliage": System.Drawing.Color.ForestGreen,
}

The #! python3 line matters if you run the file with the older RunPythonScript command. McNeel's developers have said it only runs a file as Python 3 when the file starts with that line or has a .py3 extension. System and System.Drawing are .NET namespaces, imported just like Python modules. The standard library csv module works as usual because this is CPython.

Step 2: find or make a layer

def get_layer_index(doc, name):
    index = doc.Layers.FindByFullPath(name, -1)
    if index >= 0:
        return index
    layer = Rhino.DocObjects.Layer()
    layer.Name = name
    layer.Color = LAYER_COLORS.get(name, System.Drawing.Color.Gray)
    return doc.Layers.Add(layer)

Rhino objects point at a layer by its index in the document's layer table. FindByFullPath returns that index. Its second argument is the value to return when the layer is not found; the API reference recommends -1. If it is missing we make a Layer, set its name and colour, and Layers.Add returns the new index.

Step 3: build and tag each row

def build_from_csv(doc, path):
    added = 0
    with open(path, newline="") as f:
        for row in csv.DictReader(f):
            if doc.Objects.FindByUserString("asset_id", row["asset_id"], True):
                continue  # already built on an earlier run
            center = Rhino.Geometry.Point3d(
                float(row["x"]), float(row["y"]), float(row["z"]))
            sphere = Rhino.Geometry.Sphere(center, float(row["radius"]))

            attrs = Rhino.DocObjects.ObjectAttributes()
            attrs.Name = row["name"]
            attrs.LayerIndex = get_layer_index(doc, row["category"])
            attrs.SetUserString("asset_id", row["asset_id"])
            attrs.SetUserString("category", row["category"])

            guid = doc.Objects.AddBrep(sphere.ToBrep(), attrs)
            if guid != System.Guid.Empty:
                added += 1
    return added

This is the core of the script, and it maps onto a Houdini setup like this:

  • Rhino.Geometry objects such as Sphere are plain math until you add them to the document. ToBrep() turns the sphere into a surface object that Rhino can store.
  • ObjectAttributes holds everything that is not geometry: name, layer, colour and user text. SetUserString is the closest thing Rhino has to a string attribute on a primitive.
  • doc.Objects.AddBrep(geometry, attrs) adds both at once and returns the new object's ID, or Guid.Empty if it failed.
  • The first line in the loop asks the document whether an object with this asset_id already exists. The API reference says FindByUserString returns an array of matches, or null when nothing matches; in my run it returned an empty array. Both count as false in Python, so rows that were already built are skipped either way. That makes the script safe to run again after you edit the CSV. The key and value are search patterns, so ? and * act as wildcards; keep them out of your IDs.

Step 4: run it and query the result

doc = sc.doc
count = build_from_csv(doc, CSV_PATH)
doc.Views.Redraw()
print("Added {} objects from {}".format(count, CSV_PATH))

for obj in doc.Objects.FindByUserString("category", "foliage", True) or []:
    print(obj.Attributes.Name, obj.Attributes.GetUserString("asset_id"))

sc.doc is the active document. The or [] guards against a null result. The last loop queries by user text in the same way a group expression would in Houdini: give me every object whose category is foliage.

Put the four blocks together, one after another, and you have a 55-line script. On the verified run it printed this (the path is shortened here):

Added 5 objects from C:\...\props.csv
tree_02 A005
tree_01 A004

Three layers appeared (street, furniture, foliage) in orange, steel blue and forest green. Running the script a second time printed Added 0 objects.

Running it from a terminal

You can also send the script to a running Rhino from outside. That is how this one was tested. Run the StartScriptServer command in Rhino, then in a terminal:

"C:\Program Files\Rhino 8\System\RhinoCode.exe" script C:\temp\csv_to_rhino.py

The command returns straight away with exit code 0 and prints nothing, even if the script raises an error. Rhino runs the script a moment later, and its print output goes to Rhino's command history, not to the terminal, so look for the result there. To save that text to a file, a script can write out Rhino.RhinoApp.CommandHistoryWindowText.

McNeel's RhinoCode CLI guide says the script server does not start when Rhino launches. It also suggests adding %PROGRAMFILES%\Rhino 8\System to your PATH so you can type rhinocode directly.

Going further

  • Swap the sphere for a block instance per category and you have a set-dressing loader.
  • Write the reverse: loop over doc.Objects, read GetUserString, and write a CSV back out for the next department.
  • Add a # r: pandas line under the shebang if you would rather use pandas. The script editor guide covers package requirements.

Official references: