961b3c0396
Stable working state after a long iteration session. The plugin now supports:
- Multi-Surface-Select für alle Element-Typen (Türen/Fenster/Treppen/Tragwerk)
- Wand-Z-Drag → unbound mode (UK/OK-Override, Wand vom Geschoss entkoppelt)
- Wand-Z-Drag nimmt verknüpfte Öffnungen mit (Brüstung += delta_z via Idle-Pfad)
- Öffnungs-XY-Drag snapt direktional auf Wand-Tangente
- Öffnungs-Z-Drag passt Brüstung an (Fenster sofort sync, Tür deferred)
- Wand-Delete kaskadiert Öffnungen (deferred via Idle, robust gegen _Rotate/_Move)
- Source-Cascade beim Öffnungs-Delete (deferred analog Wand-Kaskade)
- Listener-Cleanup robust gegen _reset_panels.py Reload (Refs in
_dossier_runtime_event_refs gespeichert, vor Re-Install deregistriert)
- _count_same_id_type filtert IsDeleted (verhindert Source-Duplikat-Bug bei Move)
- Frontend: Brüstungs-Slider für Tür ("Schwelle"), Flügel-Block nur bei Fenster
Plus aus früherer Phase dieser Session:
- Dossier-Launcher Auto-Load via Rhinos StartupCommands-XML
- Default-Pfad zeigt auf gebundeltes startup.py (out-of-the-box für neue User)
- Splash-Window beim Plugin-Load mit native macOS rounded corners
- Diverse Launcher-Verbesserungen (Brüstungs-Default, tauri.conf, capabilities)
Known issue: bei Multi-Select-Move mit vielen Sub-Volumen kann sporadisch
"Unable to transform" auftreten (Rhinos Move-Operation kollidiert mit Wand-
Regen). Tür-spezifischer Defer-Pfad mildert das, Fenster läuft sync.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
208 lines
11 KiB
Markdown
208 lines
11 KiB
Markdown
# Dossier — Architektur
|
|
|
|
Stand: 2026-05-17. Dieses Dokument beschreibt wie die Module zusammenspielen,
|
|
welche Konventionen gelten und wo bekannte Schwachstellen liegen.
|
|
|
|
> ⚠️ **Runtime-Realität — Migration in Arbeit:** Trotz `# ! python3`
|
|
> Shebangs in allen Files läuft der Plugin-Code aktuell als **IronPython 2.7**
|
|
> unter .NET 8 auf Mac Rhino 8.31 — verifiziert via `sys.version`. Migration
|
|
> zu Rhinos neuer CPython-3-Engine läuft (siehe CLAUDE.md für Stand). Bis
|
|
> dahin gelten IPy-2.7-Idiome im Code: `__builtin__`, kein f-string-Vertrauen,
|
|
> `print` mit/ohne Klammern gemischt, `import System` direkt (statt `clr.AddReference`).
|
|
|
|
**Bei jeder Code-Änderung in `rhino/` zuerst dieses Dokument lesen.** Wenn
|
|
sich Patterns hier ändern, dieses Dokument mit-aktualisieren — sonst rotted
|
|
es.
|
|
|
|
---
|
|
|
|
## 1. Module-Map (`rhino/`)
|
|
|
|
| Modul | LOC | Rolle | Hängt ab von |
|
|
|---|---:|---|---|
|
|
| `panel_base.py` | 697 | **Fundament**: BaseBridge, WebView-IO, Panel-Registration, Icons, Legacy-Migration | — |
|
|
| `rhinopanel.py` (EBENEN) | 798 | Zeichnungsebenen (Layer-Hierarchie, Presets) | panel_base, layer_builder, massstab (sticky) |
|
|
| `elemente.py` (ELEMENTE) | **7244** | Smart Elements: Wände, Decken, Öffnungen, Treppen, Tragwerk, Räume (SIA-416) | panel_base, overrides+rhinopanel (sticky) |
|
|
| `gestaltung.py` (GESTALTUNG) | 1635 | Selektions-Attribute: Farbe, Lineweight, Linetype, Hatch, Plot-Sync | panel_base, massstab (sticky) |
|
|
| `oberleiste.py` (OBERLEISTE) | 981 | Top-Bar: View/Display/Massstab-Proxy, Snaps, Window-Layout, Settings | panel_base, massstab, overrides (sticky) |
|
|
| `massstab.py` (MASSSTAB) | 1096 | Viewport-Skala 1:N, Auto-DPI (CoreGraphics), PlotWeight | panel_base, layer_builder |
|
|
| `overrides.py` | 797 | Engine: regelbasierte Overrides (Bedingung→Aktion), Presets cross-doc | — (Library) |
|
|
| `overrides_panel.py` | 226 | UI auf overrides-Engine | panel_base, overrides, oberleiste (sticky) |
|
|
| `ausschnitte.py` (AUSSCHNITTE) | 708 | Viewport-Snapshots (Kamera + Display + Layer) | panel_base, massstab |
|
|
| `dimensionen.py` (DIMENSIONEN) | 613 | Bemaßungs-Panel (Wand-Dicken, Geschoss-Höhen, Öffnungen) | panel_base |
|
|
| `layouts.py` (LAYOUTS) | 749 | Layout-Editor für Druckplatten | panel_base |
|
|
| `werkzeuge.py` (WERKZEUGE) | 58 | Quick-Tools (Batch-Operationen) | panel_base |
|
|
| `layer_builder.py` | 436 | Helper: Ebenen-Hierarchie aufbauen, Sublayer-Sync | — (Library) |
|
|
| `startup.py` | 136 | Initialisierer: liest `dossier.project.json`, lädt Module selektiv | panel_base + Module via `__import__` |
|
|
| `clean.py` / `clean_layers.py` / `_reset_panels.py` / `inspect_section.py` | 48-163 | Wartung / Debugging | — |
|
|
|
|
---
|
|
|
|
## 2. Tragende Patterns
|
|
|
|
### 2.1 Bridge-Pattern (Pflicht für jedes Panel)
|
|
|
|
```python
|
|
class MyBridge(panel_base.BaseBridge):
|
|
def __init__(self):
|
|
panel_base.BaseBridge.__init__(self, "mymodule")
|
|
|
|
def _on_ready(self):
|
|
self.send("STATE_SYNC", {...}) # WebView fertig geladen
|
|
|
|
def handle(self, data):
|
|
t = data.get("type")
|
|
if t == "ACTION": self._do_action()
|
|
|
|
def _bridge_factory():
|
|
b = MyBridge()
|
|
_install_listeners(b) # Rhino-Events registrieren
|
|
return b
|
|
|
|
panel_base.register_and_open(
|
|
"mymodule", "MY PANEL", PANEL_GUID_STR,
|
|
_bridge_factory,
|
|
icon_spec=("foundation", "#5fa896"), # Material-Icon-Name + Petrol
|
|
min_size=(400, 300),
|
|
)
|
|
```
|
|
|
|
### 2.2 React ↔ Python Kommunikation
|
|
|
|
- **React → Python**: `document.title = "RHINOMSG::{json}"` — gepollt im Idle-Handler in `panel_base`.
|
|
- **Python → React**: `bridge.send(type, payload)` → `webview.ExecuteScript("window.onRhinoMessage(...)")`
|
|
- **Chunking**: Messages > 200 KB werden in `panel_base.handle_raw` automatisch gesplittet + reassembliert. Subklassen kümmern sich nicht drum.
|
|
|
|
### 2.3 Sticky-Storage (Cross-Module-State)
|
|
|
|
Konventionen für Keys:
|
|
|
|
- `"{modul}_bridge"` — Bridge-Instanz (in `_bridge_factory` registriert)
|
|
- `"{modul}_listeners"` — Bool-Flag: Listener bereits registriert? (verhindert Doppel-Hook)
|
|
- `"_dossier_*"` — globale States (z.B. `_dossier_joints_cache`, `_dossier_timing_enabled`, `_dossier_layout_applied`)
|
|
- `"{modul}_*_cache"` — Modul-Cache (z.B. `_JOINTS_CACHE_KEY` in elemente)
|
|
|
|
### 2.4 Listener-Hookup (Idempotent)
|
|
|
|
```python
|
|
def _on_idle(s, e):
|
|
b = sc.sticky.get("mymodule_bridge")
|
|
if b is not None: # IMMER None-Check
|
|
try: b._send_state()
|
|
except Exception: pass
|
|
|
|
def _install_listeners(bridge):
|
|
flag = "mymodule_listeners"
|
|
sc.sticky["mymodule_bridge"] = bridge
|
|
if sc.sticky.get(flag): return # Schon registriert
|
|
Rhino.RhinoApp.Idle += _on_idle
|
|
Rhino.RhinoDoc.ActiveDocumentChanged += _on_view_change
|
|
sc.sticky[flag] = True
|
|
```
|
|
|
|
### 2.5 Cache-Pattern
|
|
|
|
- **Joint-Cache** (`elemente.py: _JOINTS_CACHE_KEY`): pro Geschoss; invalidiert bei Add/Delete/Replace.
|
|
- **Material-Cache** (`elemente.py`): Hex→MaterialIndex; stale-Check beim Lesen.
|
|
- **Hatch-Curve-Link** (`gestaltung.py`): UUID→Hatch in Sticky, weil Rhino UserStrings bei Move/Replace teils wegwischt.
|
|
- **Display-Modes-Cache** (`oberleiste.py`): einmalig gelesen, Sticky-gecacht.
|
|
- **Pending-Hatch TTL** (`gestaltung.py`): 3 s Fenster nach Drag/Move, in dem Hatch-Metadaten wiederherstellbar sind.
|
|
|
|
### 2.6 Settings-File
|
|
|
|
Pfad-Hierarchie:
|
|
|
|
1. **Primär** (Launcher schreibt): `~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json`
|
|
2. **Legacy-Fallback** (read-only): `~/Library/Application Support/RhinoPanel/dossier_settings.json`
|
|
|
|
Bekannte Keys: `windowLayout`, `autoApplyLayout`, `pendingApplyLayout`, `rhinoApp`, `templatePath`.
|
|
|
|
Normalisierung: Legacy `defaultLayout` → `windowLayout` in `_settings_load`.
|
|
|
|
### 2.7 Window-Layouts auf Mac (XML, nicht .rwl!)
|
|
|
|
- Speicherort: `~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces/<GUID>.xml`
|
|
- Display-Name aus `<RhinoUI name="...">` Attribut.
|
|
- Apply: Reflection über `Rhino.UI.WindowLayout.*`, Fallback `_-SetActiveLayout "Name" _Enter`.
|
|
- Live-Apply aus dem Launcher: setzt `pendingApplyLayout` im Settings-JSON; `oberleiste.tick_idle()` pollt + clearet.
|
|
|
|
---
|
|
|
|
## 3. Cross-Module-Pfade (Sticky-Bus)
|
|
|
|
| Sender → Empfänger | Trigger | Effekt |
|
|
|---|---|---|
|
|
| `rhinopanel` → `elemente` | Apply von Ebenen-Struktur (Höhen/OKFF) | `elemente_bridge._regenerate_all()` regeneriert Wände/Decken |
|
|
| `elemente` → `rhinopanel` | Wand/Decken-Delete | `ebenen_bridge_ref._send_state()` (Fallback-Chain) |
|
|
| `oberleiste` → `overrides` | Preset-Auswahl in Topbar | `overrides_bridge._send_state()` |
|
|
| `massstab` ↔ `ausschnitte` | Viewport-/Zoom-Wechsel | Bi-direktional Skala lesen/setzen |
|
|
| `gestaltung` ↔ `rhinopanel` | Hatch-Pattern auf Selektion | Pattern+Scale+Rotation-Signature vergleichen |
|
|
|
|
**Risiko**: Sticky-Reads ohne `is not None`-Check sind verstreut (vor allem in `oberleiste.py` an einigen Stellen). Bei Refactor immer Schutz einbauen.
|
|
|
|
---
|
|
|
|
## 4. Bekannte Schwachstellen
|
|
|
|
### 4.1 `elemente.py` Monolith (7244 LOC)
|
|
Enthält Wand-Achse+Volumen, Wand-Miter/T-Junction, Decken (Brep-Extrusion), Öffnungen (Fenster/Türen mit Rahmen+Sims+Flügel), Treppen (gerade/L/Wendel), Tragwerk (Stütze/Träger/I-Profil), Räume (SIA-416 Stempel + Farben). Vorschlag für späteren Refactor: Split in `wand.py`, `decke.py`, `oeffnung.py`, `treppe.py`, `tragwerk.py`, `raum.py` + Shared-Utils-Modul (`_read_meta`, `_geschoss_lookup`, `_active_geschoss_id`). Aktuell **nicht kritisch**, aber bremst Navigation und Tests.
|
|
|
|
### 4.2 Duplizierter Code
|
|
- `_color_to_hex` / `_hex_to_color` in 3 Modulen (gestaltung, panel_base, layer_builder)
|
|
- Geschoss-Lookup-Helper in elemente.py (gehören in layer_builder)
|
|
- Layer-Hierarchie-Aufbau split zwischen layer_builder.py und rhinopanel.py
|
|
|
|
### 4.3 Cache-Stale-Risiken
|
|
- **Joint-Cache** invalidiert bei Add/Delete/Replace — Undo/Redo wird **nicht** abgefangen
|
|
- **Material-Cache** erkennt erst beim Zugriff dass ein Index ungültig ist; bei Material-Delete in Rhino bleibt Cache bis Restart stale
|
|
- Workaround: `clean.py` für manuellen Cache-Clear, aber nicht automatisch
|
|
|
|
### 4.4 None-Check-Lücken in Sticky-Reads
|
|
Beispiel `oberleiste.py:733` (Pfad variiert):
|
|
```python
|
|
b = sc.sticky.get("overrides_bridge") # kann None sein
|
|
b._send_state() # → AttributeError wenn None
|
|
```
|
|
Beim nächsten Touch dieser Stellen: `if b is not None:` einziehen.
|
|
|
|
### 4.5 `ebenen_bridge_ref` Fallback-Chain
|
|
Drei Lookup-Namen aus historischen Gründen (`ebenen_bridge_ref` → `ebenen_bridge` → `rhinopanel_bridge`). Bei nächstem Touch konsolidieren.
|
|
|
|
### 4.6 Doppel-Listener-Risiko
|
|
Wenn zwei Module gleichzeitig laden und beide einen globalen Idle-Handler registrieren — Flag-Schutz pro Modul gut, aber kein zentrales Lock. Bisher in Praxis kein Problem.
|
|
|
|
---
|
|
|
|
## 5. Was die Architektur richtig macht (nicht anfassen ohne Grund)
|
|
|
|
1. **BaseBridge-Abstraktion** ist sauber: alle Bridges folgen demselben Lifecycle, WebView-Integration ist transparent.
|
|
2. **Chunk-Handling** für Large Messages (>200 KB) in `panel_base.handle_raw` — elegant, Subklassen merken nichts.
|
|
3. **Migration-Strategy** (`traite_` → `pause_` → `dossier_` Sticky-Prefixes): idempotent, per-Doc-Flag verhindert Mehrfach-Lauf.
|
|
4. **Icon-System**: Multi-Fallback PNG → SVG → Material-Font → Buchstabe; gecacht.
|
|
5. **Selective Module-Loading** über `startup.py` + `dossier.project.json`: ein Projekt zieht nur die benötigten Module.
|
|
6. **DPI-Auto-Detection** via CoreGraphics auf Mac — robuster als die meisten alternativen Ansätze.
|
|
7. **UTF-8 Handling konsequent**: `ensure_ascii=False`, defensive int()-Casts vor Format — vermeidet Rhino-Encoder-Bugs.
|
|
8. **Defensives Error-Handling**: Try/Except mit `[MODUL]`-Präfix-Logging in der Rhino-Konsole; Plugin bricht nicht ab bei Einzelfehlern.
|
|
|
|
---
|
|
|
|
## 6. Launcher-Anbindung (Tauri)
|
|
|
|
Der **Dossier-Launcher** (`launcher/`) ist eine separate Tauri-App:
|
|
- Verwaltet Projekt-Liste + Settings + Updates (auto via `tauri-plugin-updater`)
|
|
- Schreibt `dossier_settings.json` in den oben (§2.6) genannten Primär-Pfad
|
|
- Live-Push an laufende Rhino-Session: `pendingApplyLayout`-Key in Settings, `oberleiste.tick_idle()` pollt + clearet
|
|
- System-Tray mit Quick-Open der letzten 5 Projekte (`refresh_tray_menu`-Command nach jedem Recent-Update)
|
|
|
|
Rhino kann ohne Launcher laufen; Launcher kann ohne Rhino laufen. IPC ist bewusst dateibasiert, kein Socket.
|
|
|
|
---
|
|
|
|
## 7. Wenn du was änderst
|
|
|
|
1. **Lies dieses Dokument** + den `## tragenden Patterns`-Block der CLAUDE.md
|
|
2. **Halt dich an die Naming-Konventionen** (Sticky-Keys, Bridge-Factory)
|
|
3. **Bei Sticky-Reads: `is not None`-Check** (siehe §4.4)
|
|
4. **Cache invalidieren wenn dein Code Source-Daten ändert** (siehe §2.5)
|
|
5. **Dieses Dokument up-to-date halten** wenn sich Patterns/Schwachstellen ändern
|