Files
DOSSIER/ARCHITECTURE.md
karim 961b3c0396 Snapshot: Wand/Öffnung Multi-Surface-Select + Z-Drag + Brüstungs-Mitnahme
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>
2026-05-18 01:50:45 +02:00

11 KiB

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)

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)

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 defaultLayoutwindowLayout 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
rhinopanelelemente Apply von Ebenen-Struktur (Höhen/OKFF) elemente_bridge._regenerate_all() regeneriert Wände/Decken
elementerhinopanel Wand/Decken-Delete ebenen_bridge_ref._send_state() (Fallback-Chain)
oberleisteoverrides Preset-Auswahl in Topbar overrides_bridge._send_state()
massstabausschnitte Viewport-/Zoom-Wechsel Bi-direktional Skala lesen/setzen
gestaltungrhinopanel 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):

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_refebenen_bridgerhinopanel_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