OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac): - Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde), Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel), Treppen (gerade · L · Wendel mit Schrittmass-Validierung) - Live-Previews mit Step-Lines + Soll-Range-Clamping - Bidirektionale Selection-Sync zwischen Source-Linie und Volume - Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz - Layouts mit PDF-Export - Ausschnitte / Massstab / Override-Regeln - Petrol-Gruen Theme (Rapport-konform) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6.8 KiB
RhinoPanel — Projektdokumentation für Claude
Was ist das?
Ein React-Plugin für Rhino 8 (Mac) das als schwebendes Fenster läuft und Architektur-Workflows aus Vectorworks/ArchiCAD nachbildet. Die React-UI wird in Rhinos Eto.Forms WebView über LoadHtml (inline) eingebettet.
Kommunikation React ↔ Python
React → Python: document.title = "RHINOMSG::{json}" (queue-basiert, 80ms delay)
Python → React: webview.ExecuteScript("window.onRhinoMessage({...})")
Nachrichten-Typen:
APPLY— Ebenen auf Rhino anwenden, GH triggernLAYER_VISIBILITY— Layer sofort ein/ausblendenLAYER_LOCK— Layer sperren/entsperrenSET_ACTIVE— Aktiven Rhino-Layer setzenSTATE_SYNC— Python → React, beim Panel-Start
Datenmodell
[
{"id": "eg", "name": "EG", "type": "grundriss", "hoehe": 3.50, "schnitthoehe": 1.00, "okff": 0.00},
{"id": "1og", "name": "1OG", "type": "grundriss", "hoehe": 3.00, "schnitthoehe": 1.00, "okff": 3.50},
{"id": "saa", "name": "Schnitt A-A", "type": "schnitt"},
{"id": "nor", "name": "Nordansicht", "type": "ansicht"}
]
Gespeichert in doc.Strings["rhinopanel_ebenen"] (JSON). OKFF wird nur für type: "grundriss" berechnet (kumulativ). Schnitt/Ansicht haben keine Höhenparameter.
Rhino Layer-Hierarchie
10_GRUNDRISSE
└── EG
├── 01_WAND (schwarz, lw 0.50)
├── 02_TUER_FENSTER (blau, lw 0.25)
├── 03_MOEBEL (grau, lw 0.13)
├── 04_TEXT (hellgrau, lw 0.13)
├── 05_TREPPEN (gold, lw 0.35)
└── 06_3D_VOLUMEN (lila, lw 0.25)
└── 1OG (gleiche Sublayer)
20_SCHNITTE
└── Schnitt A-A
├── 21_PROFIL (rot, lw 0.70)
├── 22_WAND (orange, lw 0.25)
└── 23_TEXT (hellgrau, lw 0.13)
30_ANSICHTEN
└── Nordansicht
├── 31_FASSADE (türkis, lw 0.35)
└── 32_TEXT (hellgrau, lw 0.13)
00_RASTER, 01_VERMESSUNG, 40_SITUATION, 90_REFERENZEN, 99_KONSTRUKTION
Dateistruktur
rhino-panel/
├── src/
│ ├── App.jsx # Hauptkomponente, State-Management
│ ├── main.jsx # Entry point, window.onerror handler
│ ├── index.css # Dark theme, CSS-Variablen (Swiss minimal)
│ ├── lib/
│ │ └── rhinoBridge.js # Kommunikation React↔Python
│ └── components/
│ ├── EbenenManager.jsx # Zeichnungsebenen-Liste (GR/SC/AN Badges)
│ ├── EbenenDialog.jsx # Dialog zum Bearbeiten der Ebenen
│ ├── LayerPanel.jsx # Layer-Sichtbarkeit/Sperre togglen
│ ├── BottomBar.jsx # "Auf Rhino anwenden" Button
│ └── Section.jsx # Ausklappbarer Abschnitt
├── rhino/
│ ├── rhinopanel.py # Panel starten, Bridge, LoadHtml-Inline
│ ├── layer_builder.py # Rhino-Layer erstellen/aktualisieren
│ └── INSTALL.md # Setup-Anleitung inkl. GH
├── dist/ # Gebaute App (npm run build)
└── vite.config.js # base: './' wichtig für file:// URLs
Alte Dateien (nicht mehr aktiv, können gelöscht werden):
src/components/GeschossManager.jsx— ersetzt durch EbenenManagersrc/components/GeschossDialog.jsx— ersetzt durch EbenenDialog
Kritische technische Details
Warum LoadHtml statt file:// URL
Rhinos WKWebView blockiert <script type="module"> bei file:// URLs (CORS/module restrictions). Lösung: Python liest dist/index.html, inliniert CSS und JS, lädt via webview.LoadHtml(html). JS wird in DOMContentLoaded eingewickelt damit #root existiert wenn React rendert.
Warum Idle-Event für URL-Loading
LoadHtml in __init__ läuft bevor der WebView in einem Fenster ist → React kann nicht in DOM rendern. Fix: Rhino.RhinoApp.Idle Event wartet bis Fenster sichtbar ist.
Warum kein Docking
RegisterPanel schlägt fehl: "constructor must accept uint, RhinoDoc or no params". IronPython *args befriedigt Rhinos .NET-Reflection nicht. Panel läuft daher als schwebendes forms.Form.
IronPython 2 Limitierungen
# -*- coding: utf-8 -*-Header in allen .py Dateien- Keine Umlaute in Strings (ue/oe/ae statt ü/ö/ä)
e.Titlestattwebview.Titleim DocumentTitleChanged Handler
Grasshopper-Anbindung
Wenn "Anwenden" gedrückt wird:
layer_builder.build_layers()erstellt/aktualisiert Rhino-Layer- JSON wird in
doc.Strings["rhinopanel_ebenen"]gespeichert canvas.Document.NewSolution(True)triggert GH-Neuberechnung
GH Python-Komponente (eine Komponente, Output a):
import json, Rhino, Rhino.Geometry as rg
doc = Rhino.RhinoDoc.ActiveDoc
raw = doc.Strings.GetValue("rhinopanel_ebenen")
ebenen = json.loads(raw) if raw else []
a = []
for e in ebenen:
if e.get("type") != "grundriss": continue
idx = doc.Layers.FindByFullPath("10_GRUNDRISSE::{}::01_WAND".format(e["name"]), -1)
if idx < 0: continue
for obj in doc.Objects.FindByLayer(doc.Layers[idx]):
crv = obj.Geometry
if not isinstance(crv, rg.Curve): continue
c = crv.DuplicateCurve()
c.Transform(rg.Transform.Translation(0, 0, e.get("okff", 0)))
ext = rg.Extrusion.Create(c, e["hoehe"], c.IsClosed)
if ext: a.append(ext.ToBrep())
Für Live-Updates: GH Timer Komponente (1000ms) mit Python-Komponente verbinden.
Design-System
Dark theme, Swiss minimal, inspiriert von rapport.kgva.ch.
CSS-Variablen in index.css. Wichtigste:
--bg-base: #1c1c1e— Hintergrund--accent: #5a9e5a— Grün (Anwenden-Button)--active: #3a5f8a— Blau (aktive Ebene)--font: 'Inter'— via Google Fonts
Typ-Badges: GR=blau #3a6fa8, SC=orange #c87050, AN=türkis #50c8a0
Workflow Panel starten
npm run build # nach Code-Änderungen
In Rhino (bei Änderungen am Python-Code):
import scriptcontext as sc
sc.sticky["rhinopanel_registered"] = False
sc.sticky["rhinopanel_form"] = None
→ _RunPythonScript → rhinopanel.py
Nächste mögliche Schritte
- Docking: Rhino 8 RhinoCode (Python 3) statt IronPython verwenden — hat andere Panel-Registration API
- Live-Vorschau: Rhino
ObjectAdded/ObjectModifiedEvents in rhinopanel.py für automatischen GH-Trigger ohne Anwenden-Button - Schnittlinien: Für SC-Ebenen Schnittlinie/Position als Parameter hinzufügen
- Layerpanel dynamisch: LayerPanel.jsx zeigt aktuell statische Layer-Gruppen — soll dynamisch aus
ebenen-State kommen - 2D-Fills: Schraffuren/Füllungen für Wände (Vectorworks-ähnlich)
- Viewport-Zuweisung: Grundrisse/Schnitte/Ansichten einem Rhino-Detailbild zuweisen