Jedes Dachflächengesicht getaggt: Neigung, Prozent, Abfluss – 69 Zeilen Rhino Python
69-Zeilen-Skript für Rhino 8: Dachflächennormalen auslesen, Neigung in Grad und Prozent automatisch taggen. Flache Flächen rot hervorgehoben.
Der Dachplan ist donnerstags fällig, und jemand im Büro hat den Job, den niemand in den Lebenslauf schreibt. Sie klicken eine Dachfläche an, messen sie, rechnen den Winkel aus, tippen »30°« neben einen kleinen Pfeil und gehen zur nächsten Fläche. Bei einem Walmdach mit zwei Gauben sind das ein Dutzend sorgfältige Klicks. Bei einem Wohnblock mit sechs Dachniveaus und ein paar Terrassen ist das ein Nachmittag. Dann ändert sich das Modell und die Labels sind plötzlich nicht mehr wahr.
Der Dachplan interessiert sich nicht dafür, wie müde deine Maus-Hand ist.
Dieses Tutorial ersetzt diesen Nachmittag durch eine Idee und 69 Zeilen Python. Jede ebene Dachfläche hat einen Normalvektor, den Pfeil, der senkrecht auf ihr steht. Teile diesen Pfeil in einen horizontalen und einen vertikalen Teil. Der horizontale Teil geteilt durch den vertikalen Teil ist das Gefälle in Prozent. Das Arkustangens desselben Verhältnisses ist die Neigung in Grad. Die Richtung, in die der horizontale Teil im Grundriss zeigt, ist die Richtung, in die das Wasser fliesst. Ein Vektor, drei Zahlen, jede Fläche auf einmal.
←HEUTE: Rhino 8 liefert einen ScriptEditor, der nativ CPython 3 neben RhinoCommon ausführt, also ist ein Büro-Skript eine Datei und ein Run-Button.
→3012: Die Dächer, die reparierbar bleiben, sind die, deren Zeichnungen sich selbst in Klartext erklären, den ein Fremder noch öffnen kann.
Drehpunkt: Ein lesbares 69-Zeilen-Skript ist zugleich heutige Zeitsparnis und künftige Dokumentation des Planungsprozesses.
Das Werkzeug: Rhino 8’s ScriptEditor, mit RhinoCommon (der .NET Geometry Library in Rhino) und rhinoscriptsyntax (die benutzerfreundliche Kommandoschicht darauf), alle von Robert McNeel & Associates. RhinoPython begann als Steve Baers Arbeit bei McNeel, und Rhino 8 fügte CPython 3 hinzu, das Python, das Ingenieure überall sonst verwenden, neben dem älteren IronPython 2. Das Paket ist das Geschenk hier: McNeel’s Entwickler-Docs beschreiben jeden Aufruf, den dieses Skript nutzt, sodass du von den Leuten lernst, die es gebaut haben. PAZ Academy ist ein Rhino-Zentrum in der Schweiz und ein Reseller, also wenn dein Büro noch Rhino 8 Lizenzen braucht, können wir auch dabei helfen.
Einrichtung:
# Rhino 8 Kommandozeile
ScriptEditor
# Im ScriptEditor-Fenster:
# Neue Datei -> wähle Python 3
# Füge das 69-Zeilen-Skript unten ein
# Speichern unter -> roof_pitch_tags.py
# Klicke Run, wähle das Dach, drücke Enter
Erste Schritte:
- Öffne ein beliebiges Modell mit einem Dach aus Flächen, Mehrflächigen oder Extrusionen. Eine Kopie des aktuellen Projekts ist ideal.
- Führe das Skript aus – es fragt dich, Objekte zu wählen; auch vorausgewählte Dächer funktionieren.
- Schau dir die neue Ebene
A-ROOF-PITCHan. Jede Fläche trägt einen Punkt wie30.0° | 57.7 % | S, grün, orange oder rot gefärbt. - Ändere das Modell, lösche die Punkte auf der Ebene, führe es erneut aus. Das ist der gesamte Neuausgabe-Workflow.
Eine offene Anmerkung: Die reine Mathematik-Funktion pitch() wurde für diesen Text in reinem Python 3.10 ausgeführt. Die Rhino-Hälfte wurde Zeile für Zeile gegen McNeel’s Dokumentation überprüft, aber nicht innerhalb von Rhino ausgeführt. Führe deinen ersten Test-Durchlauf auf einer Kopie durch.
Der Code, erklärt:
#! python 3
"""Roof pitch tags - Rhino 8 ScriptEditor (Python 3).
Select roof surfaces/polysurfaces. Every face gets a text dot with its
pitch in degrees and percent and the direction water runs off."""
import math
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc
from System.Drawing import Color
MIN_FALL = 1.5 # % - flacher als dies wird gekennzeichnet (auf deine Spezifikation setzen)
WALL = 89.0 # deg - steiler als dies ist eine Wand, überspringe
LAYER = "A-ROOF-PITCH"
COMPASS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
def pitch(nx, ny, nz):
"""Unit normal -> (degrees, percent, run-off direction)."""
if nz < 0: # face points down: flip it, same plane
nx, ny, nz = -nx, -ny, -nz
run = math.hypot(nx, ny) # horizontal part of the normal
deg = math.degrees(math.atan2(run, nz))
pct = 100.0 * run / nz if nz > 1e-9 else float("inf")
if run < 1e-9:
return deg, pct, "-"
az = math.degrees(math.atan2(nx, ny)) % 360.0 # 0 = north (+Y), clockwise
return deg, pct, COMPASS[int((az + 22.5) // 45) % 8]
def colour_for(deg, pct):
if pct < MIN_FALL:
return Color.FromArgb(220, 40, 40) # ponding risk
if deg < 10.0:
return Color.FromArgb(240, 160, 0) # low pitch: check build-up
return Color.FromArgb(40, 150, 60) # pitched
def main():
ids = rs.GetObjects("Select roof surfaces or polysurfaces", 8 | 16, preselect=True)
if not ids:
return
if not rs.IsLayer(LAYER):
rs.AddLayer(LAYER, Color.FromArgb(90, 90, 90))
tol = sc.doc.ModelAbsoluteTolerance
rs.EnableRedraw(False)
count = 0
for gid in ids:
brep = Rhino.Geometry.Brep.TryConvertBrep(sc.doc.Objects.FindId(gid).Geometry)
if brep is None:
continue
for face in brep.Faces:
mp = Rhino.Geometry.AreaMassProperties.Compute(face.DuplicateFace(False))
if mp is None:
continue
ok, u, v = face.ClosestPoint(mp.Centroid)
if not ok:
continue
n = face.NormalAt(u, v)
n.Unitize()
deg, pct, falls = pitch(n.X, n.Y, n.Z)
if deg > WALL:
continue
curved = "" if face.IsPlanar(tol) else "~"
text = "{}{:.1f}° | {:.1f} % | {}".format(curved, deg, pct, falls)
dot = rs.AddTextDot(text, face.PointAt(u, v))
rs.ObjectLayer(dot, LAYER)
rs.ObjectColor(dot, colour_for(deg, pct))
count += 1
rs.EnableRedraw(True)
print("{} roof faces tagged on layer {}".format(count, LAYER))
main()Zeilen 1–14 — die Einstellungen, die du besitzt: die Importe bringen Math, RhinoCommon (Rhino), rhinoscriptsyntax (rs), das Live-Dokument (scriptcontext) und .NET-Farben. Dann vier Konstanten. MIN_FALL ist das Gefälle, unter dem eine Fläche gekennzeichnet wird; 1,5 % ist ein Platzhalter, stelle es also auf die Spezifikation deines Projekts, nicht auf eine Zahl aus dem Gedächtnis. WALL überspringt alles steileres als 89°, damit Fassaden und Brüstungen still bleiben. Eine Konstante ist einfach ein benannter Wert, den du einmal oben setzt, sodass niemand nach ihr in der Logik suchen muss.
Zeilen 16–26 — die Zahl, die die ganze Arbeit verrichtet: pitch() nimmt die drei Komponenten des Normalvektors. math.hypot(nx, ny) ist die horizontale Länge, der »Lauf«. atan2(run, nz) verwandelt Lauf gegen Anstieg in Grad, und 100 * run / nz ist der Prozentsatz. Die Azimut-Zeile misst den Grundrisswinkel im Uhrzeigersinn von +Y aus, dann rundet ihn auf einen von acht Kompass-Sektoren von 45°. Eine ebene Fläche hat keinen horizontalen Teil, also gibt sie stattdessen »-« zurück, anstatt eine Richtung zu erfinden. Die ersten beiden Zeilen flippen einen nach unten zeigenden Normalvektor, weil eine Fläche und ihr gespiegeltes Gegenstück die gleiche Ebene sind.
Wenn es in reinem Python läuft, gibt die Funktion aus:
30-Grad-Dach nach Süden fallend 30.0 deg | 57.7 % | S
diesselbe Fläche, Normal geflippt 30.0 deg | 57.7 % | S
Flachdach, 2 % Fall nach Osten 1.1 deg | 2.0 % | E
Flachdach, 1 % Fall nach Westen 0.6 deg | 1.0 % | W
ebene Platte 0.0 deg | 0.0 % | -
45-Grad Walmdach nach Nordosten 45.0 deg | 100.0 % | NEZeilen 28–33 — drei Farbklassen: rot unterhalb deines Minimumgefälles, orange unter 10° als Hinweis, die Aufbauweise zu überprüfen, grün für geneigte Flächen. Color.FromArgb erstellt eine Farbe aus Rot-, Grün- und Blauwerten zwischen 0 und 255.
Zeilen 35–43 — Auswahl und Hausarbeit: nach McNeel’s RhinoScriptSyntax-Referenz nimmt rs.GetObjects einen Filter, wobei Surface 8 ist und Polysurface 16, also 8 | 16 akzeptiert beide. Die Ebene wird nur erstellt, wenn rs.IsLayer sagt, dass sie fehlt. rs.EnableRedraw(False) stoppt die Viewport-Neuzeichnung nach jedem Punkt, und das ist, wo die Geschwindigkeit herkommt. Eine Sache, die man vor dem ersten Durchlauf wissen sollte: Bei einem geschlossenen Dach-Solid besucht die Schleife auch die nach unten zeigenden Flächen, also eine flache Platte bekommt auch ihren roten 0.0 % | - Punkt auf der Unterseite. Wähle die Dachflächen aus, oder lösche die Punkte auf der Unterseite.
Zeilen 44–56 — Fläche für Fläche, die Mitte finden: Brep.TryConvertBrep verwandelt das, was du ausgewählt hast, Extrusions eingeschlossen, in einen Brep (Boundary Representation: ein Solid oder eine Hülle aus getrimten Flächen). Für jede Fläche gibt AreaMassProperties.Compute auf face.DuplicateFace(False) den Flächenschwerpunkt. ClosestPoint zieht diesen Schwerpunkt auf die Fläche als Oberflächenparameter u, v zurück, und NormalAt(u, v) liest den Normalvektor dort. Der Anfänger-Fehler sitzt auf Zeile 52: Nach der RhinoCommon-Dokumentation gibt Surface.ClosestPoint true bei Erfolg zurück und übergibt u und v als Out-Parameter zurück, und in Rhino 8 CPython kommen diese als ein Tupel (ok, u, v) an. Schreibe u, v = ... und Python stoppt mit einem Unpack-Fehler.
Zeilen 57–67 — der Punkt: IsPlanar(tol) überprüft die Ebenheit bei der Modell-Toleranz und stellt »~« vor gekrümmte Flächen. rs.AddTextDot platziert die Bezeichnung, dann rs.ObjectLayer und rs.ObjectColor sortieren und färben sie. Die Neuzeichnung kommt zurück, und die Kommandozeile meldet, wie viele Flächen gekennzeichnet wurden.
Zwei Grenzen zum Weitersagen ans Team. Der Plan-Norden ist +Y, also braucht eine rotierte Baustelle ein rotiertes Modell oder einen angepassten Kompass. Eine gekrümmte Fläche wird nur an ihrem Schwerpunkt gelesen, und auf einer L-förmigen Fläche kann der Punkt knapp ausserhalb der zugeschnittenen Kante sitzen. Der Kompromiss ist klar: ein Punkt pro Fläche ist schnell und lesbar, aber es ist eine einzelne Stichprobe, keine Gefälleabbildung eines verformten Dachs.
Atelier: Für ein 12-Personen-Studio in Zürich, das für jede Bauleitung-Runde Dachpläne ausgibt, hören die Labels auf, eine lästige Aufgabe zu sein, die am Ende jemand macht, und werden zu einer Kontrolle, die jeder zuerst ausführt – was auch bedeutet, dass die roten Flächen sichtbar werden, bevor der Dachdecker sie findet. Montagszug: roof_pitch_tags.py in den gemeinsamen Skript-Ordner des Büros legen und das vereinbarte Minimumgefälle des Projekts zu Anfang jedes Jobs in MIN_FALL schreiben.
Hack: Gib dem Baustellen-Team die Zahl, mit der sie tatsächlich ausgezogen sind: Millimeter Fall pro Meter. Prozent mal zehn ist mm pro Meter, also sollte das 2 % nach Osten fallende Dach aus dem Test 20 mm/m und die 30°-Fläche etwa 577 mm/m anzeigen. Ersetze Zeile 61 mit diesem Code und sage vorher, was er tun wird:
mm = 10 * pct # 1 % Fall = 10 mm pro Meter Lauf
text = "{}{:.1f}° | {:.1f} % | {:.0f} mm/m | {}".format(
curved, deg, pct, mm, falls)PAZ argumentierte in seinem PAZ-Rhino-Text, dass die besten Büro-Skripte mit der Aufgabe beginnen, die du jede Woche wiederholst. Dachneigungen sind diese Aufgabe. Speichere das Skript, führe es auf deinem aktuellen Dach aus und gib den nächsten Plan mit Labels aus, die das Modell selbst geschrieben hat.
Lern-Material:
- RhinoScriptSyntax-Referenz — GetObjects, AddTextDot, ObjectColor, AddLayer, EnableRedraw
- RhinoCommon BrepFace — NormalAt, PointAt, IsPlanar, DuplicateFace
- Surface.ClosestPoint — der Out-Parameter-Aufruf hinter Zeile 52
- AreaMassProperties.Compute — wo der Schwerpunkt herkommt
- PAZ-Hinweis: bring die Rhino-Aufgabe, die du jede Woche wiederholst
PAZ Kaffi · interdisziplinäre Redaktionsarbeit, geleitet von der PAZ Academy