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>
This commit is contained in:
2026-05-18 01:50:45 +02:00
parent 1180d7bedf
commit 961b3c0396
52 changed files with 10760 additions and 765 deletions
+207
View File
@@ -0,0 +1,207 @@
# 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
+126 -139
View File
@@ -1,160 +1,147 @@
# RhinoPanel — Projektdokumentation für Claude # Dossier — Projekt-Anweisungen für Claude
## Was ist das? ## Was das ist
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. **Dossier** ist ein Rhino 8 Plugin (Mac) mit React-WebView-Panels für
architektonische Workflows (Wände, Decken, Öffnungen, Räume, SIA-416,
Plan-Layouts). Teil der OpenStudio-Suite. Schwester-App: Rapport.
## Kommunikation React ↔ Python **Dossier-Launcher** ist eine separate Tauri-App (`launcher/`), die Projekte
verwaltet, Settings hält (auch für den Plugin), Auto-Updates liefert und im
System-Tray lebt.
**React → Python:** `document.title = "RHINOMSG::{json}"` (queue-basiert, 80ms delay) ## Runtime: Python 3.9 CPython (verifiziert 2026-05-17)
**Python → React:** `webview.ExecuteScript("window.onRhinoMessage({...})")`
Nachrichten-Typen: Dieser Ordner `DOSSIER/` läuft mit Rhinos **neuem Python-3-Engine** (CPython
- `APPLY` — Ebenen auf Rhino anwenden, GH triggern 3.9.10 via Script Editor). Der Vorgänger `rhino-panel/` bleibt **frozen** als
- `LAYER_VISIBILITY` — Layer sofort ein/ausblenden IPy-2.7-Referenz.
- `LAYER_LOCK` — Layer sperren/entsperren
- `SET_ACTIVE` — Aktiven Rhino-Layer setzen
- `STATE_SYNC` — Python → React, beim Panel-Start
## Datenmodell **Wie geladen wird (WICHTIG):**
-**`_ScriptEditor`** → Datei öffnen + Run-Button → CPython 3.9 (Shebang
`#! python3` wird respektiert). Das ist der **funktionierende Pfad**.
- ✅ **Rhino Options → General → Command Lists → „Run these commands every
time a model is opened"** → Form: `_-RunPythonScript "/voller/pfad.py"`
(mit Dash + Quotes). Persistiert in `Options/General/StartupCommands` der
`settings-Scheme__Default.xml`. Lädt das Skript bei jedem Rhino-Start
silent, ohne File-Dialog. Trotz Dash bleibt Shebang `#! python 3`
wirksam → CPython 3.9 (verifiziert 2026-05-17 Mac Rhino 8). Der Launcher
trägt diesen Eintrag automatisch ein.
- ⚠️ `_-RunPythonScript "path.py"` mit Dash in der **interaktiven Command-
Line** → IronPython 2.7 (Legacy). NICHT benutzen für DOSSIER, sonst
crasht die SectionStyle-Logik etc.
- ⚠️ `_RunPythonScript path.py` ohne Dash — in der Command-Line OK (Py3
via Shebang, ohne Quotes, in einer Zeile). Im StartupCommands-Feld
öffnet diese Form aber einen File-Dialog statt zu laufen.
```json **Migration-Stand:**
[ - ✅ Repo kopiert nach `DOSSIER/`, alle Pfade umgestellt
{"id": "eg", "name": "EG", "type": "grundriss", "hoehe": 3.50, "schnitthoehe": 1.00, "okff": 0.00}, - ✅ Shebangs aller Files auf `#! python3` (ohne Space) — Format das Rhino erkennt
{"id": "1og", "name": "1OG", "type": "grundriss", "hoehe": 3.00, "schnitthoehe": 1.00, "okff": 3.50}, - ✅ Code war bereits Py3-syntax-kompatibel (kein xrange/iteritems/unicode())
{"id": "saa", "name": "Schnitt A-A", "type": "schnitt"}, - ✅ Plugin läuft als CPython 3.9.10, alle 9 Panels registrieren
{"id": "nor", "name": "Nordansicht", "type": "ansicht"} - ✅ Reflection.Emit + Eto.Forms + Bridge funktionieren
] -**`Rhino.DocObjects.SectionStyle()` instanziierbar** + `layer.SetCustomSectionStyle()`
verfügbar — die volle Section-Style-API ist jetzt zugänglich (Anlass der Migration)
## Bei Code-Arbeit — Reihenfolge
1. **`ARCHITECTURE.md` zuerst lesen** — Module-Map, Konventionen, Schwachstellen.
2. **Dann das relevante Modul lesen**. Nicht raten was drin steht.
3. **Erst danach editieren**.
## Anti-Over-Engineering
Diese Regeln sind nicht verhandelbar — Verstöße kosten Zeit beim Aufräumen.
- **Keine Abstraktionen einführen, die ein konkretes Problem lösen.**
Drei ähnliche Zeilen sind besser als eine Hilfsfunktion mit drei Aufrufstellen.
- **Kein „aufräumen drumherum".** Ein Bugfix bleibt ein Bugfix.
- **Kein Error-Handling für Szenarien, die nicht eintreten können.**
- **Keine Feature-Flags, keine Migrations-Shims parallel zur alten Funktion.**
- **Keine Kommentare die WAS sagen** — nur WARUM-Kommentare wenn nicht-offensichtlich.
- **Keine erfundenen Module/Funktionen/Flags.** Erst `grep`, dann editieren.
## Anti-Patterns (aus echten Sessions)
- **`try/except: pass` als Bug-Verstecker.** Lieber `print("[MODUL] err:", ex)` und weiter.
- **Sticky-Reads ohne `is not None`-Check.** Siehe `ARCHITECTURE.md §4.4`.
- **„Cleveres" Refactoring von Wand-Geometrie.** `elemente.py` (7244 LOC)
enthält BIM-Logik die in Echtbau-Projekten läuft. NIE in einem Rutsch
modularisieren ohne expliziten Auftrag + Test-Plan.
- **Mac vs. Windows Rhino-Pfade verwechseln.** Mac Rhino 8 speichert
Window-Layouts als **XML in `Scheme__Default/workspaces/<GUID>.xml`**, nicht
als `.rwl`.
- **Python-Runtime annehmen statt prüfen.** Diagnose mit `print(sys.version)`
— das hat uns 4 Wochen versteckte IPy2.7 gekostet.
- **`document.title` mit Umlauten kaputt machen.** UI-Strings dürfen Umlaute;
**Python-Backend bevorzugt `ue/oe/ae`** in Identifiern, Layer-Codes,
UserString-Values.
## Python-Konventionen — POST-MIGRATION TARGET
Sobald die Migration durch ist, gilt:
- Datei-Header: `# ! python3` + `# -*- coding: utf-8 -*-` (werden dann wirksam)
- **Aufruf in Rhino:** `_RunPythonScript "path"` (ohne Dash!) — sonst startet
IPy 2.7. Alternative: `_-ScriptEditor` mit Code-Engine
- `print(x)` — IMMER mit Klammern (Python-3-Style)
- `builtins` statt `__builtin__`
- f-strings erlaubt: `f"value: {x}"`
- CLR: `import clr; clr.AddReference("System.Drawing"); from System.Drawing import Color`
- UI-Strings dürfen Umlaute, Code-Identifier nicht (`tuer`, nicht `tür`)
## Build & Reset (Cheatsheet)
```bash
# Rhino-Panels Frontend
npm run build # im Repo-Root
# Launcher Frontend
cd launcher && npm run build
# Launcher Backend Check
cd launcher/src-tauri && cargo check
# Launcher Release (signiert, schreibt latest.json)
cd launcher && ./scripts/release.sh
# Python-Syntax-Check (kein Rhino nötig)
python3 -c "import ast; ast.parse(open('rhino/elemente.py').read())"
# Runtime-Verify in Rhino (welcher Python-Engine läuft wirklich?)
_RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/startup.py
# Erste Zeile sollte zeigen: [STARTUP] Python: 3.x ... nach Migration
``` ```
Gespeichert in `doc.Strings["rhinopanel_ebenen"]` (JSON). OKFF wird nur für `type: "grundriss"` berechnet (kumulativ). Schnitt/Ansicht haben keine Höhenparameter. **Plugin reset in Rhino** (nach Python-Änderungen):
## 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 EbenenManager
- `src/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.Title` statt `webview.Title` im DocumentTitleChanged Handler
## Grasshopper-Anbindung
Wenn "Anwenden" gedrückt wird:
1. `layer_builder.build_layers()` erstellt/aktualisiert Rhino-Layer
2. JSON wird in `doc.Strings["rhinopanel_ebenen"]` gespeichert
3. `canvas.Document.NewSolution(True)` triggert GH-Neuberechnung
**GH Python-Komponente (eine Komponente, Output `a`):**
```python ```python
import json, Rhino, Rhino.Geometry as rg _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_reset_panels.py
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 ## Design-System
Dark theme, Swiss minimal, inspiriert von rapport.kgva.ch. - Petrol-Grün `--accent: #5fa896` (Hauptakzent), dunkles Petrol `#2f5d54`
CSS-Variablen in `index.css`. Wichtigste: - Hintergrund `--bg: #0e1413`
- `--bg-base: #1c1c1e` — Hintergrund - Fonts: Krungthep/Archivo Black für „DOSSIER"-Logo, Playfair Display für
- `--accent: #5a9e5a` — Grün (Anwenden-Button) Headings, DM Mono für Body
- `--active: #3a5f8a` — Blau (aktive Ebene) - Konsistent zur Website (`/Users/karim/STUDIO/DOSSIER-WEBSITE/`)
- `--font: 'Inter'` — via Google Fonts
Typ-Badges: GR=blau `#3a6fa8`, SC=orange `#c87050`, AN=türkis `#50c8a0` ## Settings-Files (Pfade)
## Workflow Panel starten - **Launcher → Rhino IPC** (file-based, kein Socket):
`~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json`
- Legacy-Fallback (read-only): `~/Library/Application Support/RhinoPanel/dossier_settings.json`
- Launcher-Cache: `~/Library/Application Support/ch.gabrielevarano.Dossier/recent.json`
```bash ## Vorgänger-Codebase
npm run build # nach Code-Änderungen
```
In Rhino (bei Änderungen am Python-Code): Der alte Ordner `/Users/karim/STUDIO/rhino-panel/` bleibt als **read-only
```python Referenz** stehen. Wenn ein Feature dort funktioniert das hier noch nicht
import scriptcontext as sc portiert ist — dort schauen, dann hier neu in Python-3-Style implementieren.
sc.sticky["rhinopanel_registered"] = False **Nicht** einfach kopieren, sondern beim Übertragen den Migrations-Style
sc.sticky["rhinopanel_form"] = None anpassen.
```
`_RunPythonScript``rhinopanel.py`
## Nächste mögliche Schritte ## Wenn unklar — fragen, nicht raten
- **Docking**: Rhino 8 RhinoCode (Python 3) statt IronPython verwenden — hat andere Panel-Registration API Wenn die Aufgabe ambig ist oder Konsequenzen über die offensichtlichen
- **Live-Vorschau**: Rhino `ObjectAdded`/`ObjectModified` Events in rhinopanel.py für automatischen GH-Trigger ohne Anwenden-Button hinausgehen: erst nachfragen.
- **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
+1 -1
View File
@@ -48,7 +48,7 @@ In Rhino 8 das Hauptpanel über `_RunPythonScript` öffnen:
```python ```python
# Hauptmenu # Hauptmenu
_RunPythonScript "/Users/karim/STUDIO/rhino-panel/rhino/rhinopanel.py" _RunPythonScript "/Users/karim/STUDIO/DOSSIER/rhino/rhinopanel.py"
``` ```
Bei Änderungen am Python-Code Panels neu laden: Bei Änderungen am Python-Code Panels neu laden:
+1 -1
View File
@@ -25,7 +25,7 @@ Damit die Module bei jedem Rhino-Start automatisch laden:
2. `Rhinoceros 8``Preferences``General`**Startup commands** 2. `Rhinoceros 8``Preferences``General`**Startup commands**
3. Folgende Zeile eintragen: 3. Folgende Zeile eintragen:
``` ```
_-RunPythonScript "/Users/karim/STUDIO/rhino-panel/rhino/startup.py" _-RunPythonScript "/Users/karim/STUDIO/DOSSIER/rhino/startup.py"
``` ```
4. OK → Rhino neu starten 4. OK → Rhino neu starten
+3
View File
@@ -4,6 +4,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dossier</title> <title>Dossier</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=Playfair+Display:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+5 -5
View File
@@ -1,11 +1,11 @@
{ {
"version": "0.1.0", "version": "0.6.3",
"notes": "Dossier 0.1.0", "notes": "Silent Plugin Auto-Load via _-RunPythonScript + korrigierter Shebang #! python 3 — kein ScriptEditor mehr",
"pub_date": "2026-05-17T01:00:34Z", "pub_date": "2026-05-17T14:26:39Z",
"platforms": { "platforms": {
"darwin-aarch64": { "darwin-aarch64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVRNFYzbUN3TE44QnZxVWhTaXhKKzZrUnBPTzA5NSt4TGduMlFZam9qN2RnRXlvYU9iOVFBa2NTbVRGNjF1TUNNeE5CeEgrTTh3S0pUVnlGTytOSngyTWRhYjEyZXBMNVFVPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc4OTc5NjM0CWZpbGU6RG9zc2llci5hcHAudGFyLmd6CmZYUGlwaXZ6Zkhpdittcm5PN04rMVVhSENlaW1GMFZ0WHh3VUlEc01LT0kwaUJNTzBIeS9OTUNjTTZ5ZVBlYyt5VFlqZThRVjZ6OXRnaXBydDlxOUJRPT0K", "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVRNFYzbUN3TE44QnNuNmhvcmJZcTdFbklSYllMcmQvYTI0NDhVRFlPWGN0YXFmSWxXSW9XSE1zbmJTWEpRdTZSc0xlcFg1VVZ5bGUvaEhSTGlhL2NjNi96NzVQZ0R0bWc4PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc5MDI3OTk5CWZpbGU6RG9zc2llci5hcHAudGFyLmd6CjkzQXZKMDlBbndqeTlHa2hBY2NwUGJNckJXZXNCbWhNRkZ2bW9VeDlSZ0JiK1lVRzBVTHdqK0V5T0NXNFlBMWdJRXRHRStKWnVtNG1WcWx2T1pkMUNnPT0K",
"url": "https://git.kgva.ch/karim/DOSSIER/releases/download/0.1.0/Dossier.app.tar.gz" "url": "https://git.kgva.ch/karim/DOSSIER/releases/download/0.6.3/Dossier.app.tar.gz"
} }
} }
} }
+22 -2
View File
@@ -1,15 +1,17 @@
{ {
"name": "dossier-launcher", "name": "dossier-launcher",
"version": "0.1.0", "version": "0.5.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dossier-launcher", "name": "dossier-launcher",
"version": "0.1.0", "version": "0.5.0",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.0.0", "@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0", "@tauri-apps/plugin-dialog": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-updater": "^2.0.0",
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6" "react-dom": "^19.2.6"
}, },
@@ -1058,6 +1060,24 @@
"@tauri-apps/api": "^2.11.0" "@tauri-apps/api": "^2.11.0"
} }
}, },
"node_modules/@tauri-apps/plugin-process": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz",
"integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-updater": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz",
"integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.10.1"
}
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+3 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "dossier-launcher", "name": "dossier-launcher",
"private": true, "private": true,
"version": "0.1.0", "version": "0.6.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -12,6 +12,8 @@
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.0.0", "@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0", "@tauri-apps/plugin-dialog": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-updater": "^2.0.0",
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6" "react-dom": "^19.2.6"
}, },
+146
View File
@@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Dossier lädt</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&display=swap" rel="stylesheet" />
<style>
:root {
--accent: #5fa896;
--accent-soft: #6fb5a3;
--accent-deep: #2f5d54;
--paper: #ffffff;
--paper-mute: rgba(255, 255, 255, 0.72);
--paper-faint: rgba(255, 255, 255, 0.45);
--font-display: Krungthep, 'Archivo Black', sans-serif;
--font-mono: 'DM Mono', 'Menlo', monospace;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: transparent !important;
color: var(--paper);
overflow: hidden;
font-family: var(--font-mono);
user-select: none;
-webkit-user-select: none;
cursor: default;
}
.frame {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 24px 28px 22px;
display: grid;
grid-template-rows: auto 1fr auto;
gap: 0;
background:
radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
border-radius: 16px;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.18);
}
.brand-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.brand {
font-family: var(--font-display);
font-size: 30px;
letter-spacing: -0.01em;
line-height: 1;
color: var(--paper);
}
.brand-dot {
color: var(--accent-deep);
}
.version {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.10em;
color: var(--paper-mute);
text-transform: uppercase;
}
.status-row {
align-self: end;
display: flex;
align-items: center;
gap: 10px;
margin-top: 20px;
font-size: 11px;
letter-spacing: 0.10em;
color: var(--paper);
text-transform: uppercase;
}
.dot-pulse {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--paper);
animation: pulse 1.6s ease-out infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255,255,255,0.55); transform: scale(1); }
70% { box-shadow: 0 0 0 9px rgba(255,255,255,0); transform: scale(1.05); }
100% { box-shadow: 0 0 0 0 rgba(255,255,255,0); transform: scale(1); }
}
.bar {
position: relative;
height: 2px;
width: 100%;
background: rgba(255, 255, 255, 0.18);
border-radius: 2px;
overflow: hidden;
margin-top: 14px;
}
.bar::after {
content: "";
position: absolute;
top: 0;
left: -35%;
width: 35%;
height: 100%;
background: linear-gradient(90deg, transparent, var(--paper), transparent);
animation: slide 1.6s linear infinite;
}
@keyframes slide {
to { left: 100%; }
}
.meta-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-top: 12px;
font-size: 9.5px;
letter-spacing: 0.14em;
color: var(--paper-faint);
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="frame">
<div class="brand-row">
<div class="brand">DOSSIER<span class="brand-dot">.</span></div>
<div class="version">v0.6.3</div>
</div>
<div>
<div class="status-row">
<span class="dot-pulse"></span>
<span>Plugin lädt — Panels werden platziert</span>
</div>
<div class="bar"></div>
</div>
<div class="meta-row">
<span>AGPL-3.0 · Karim Gabriele Varano</span>
<span>Rhino 8 · CPy 3.9</span>
</div>
</div>
</body>
</html>
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Build + sign Dossier-Launcher fuer den Updater, und emit latest.json.
#
# Voraussetzungen:
# - Private Key liegt unter ~/.tauri/dossier_updater.key
# (einmalig erzeugen mit:
# npx tauri signer generate -w ~/.tauri/dossier_updater.key)
# - Version wurde in src-tauri/tauri.conf.json + package.json hochgezaehlt
#
# Ablauf:
# 1) npx tauri build (mit Signing-Env)
# 2) liest die erzeugte .sig-Datei
# 3) schreibt latest.json im launcher-Root mit URLs auf Gitea-Release-Assets
#
# Danach manuell:
# - auf Gitea einen Release mit Tag <VERSION> erstellen
# - die .app.tar.gz und (optional) die .dmg als Assets hochladen
# - latest.json committen + auf main pushen
set -euo pipefail
cd "$(dirname "$0")/.."
KEY_PATH="${TAURI_SIGNING_PRIVATE_KEY_PATH:-$HOME/.tauri/dossier_updater.key}"
GITEA_REPO="https://git.kgva.ch/karim/DOSSIER"
# WICHTIG: `npx tauri ...` ohne Namespace zieht ausserhalb dieses Verzeichnisses
# das uralte tauri@0.15 von npm (das kein `signer` kennt). Wir nutzen den
# expliziten Paketnamen @tauri-apps/cli — der funktioniert von ueberall und
# auch innerhalb dieses Verzeichnisses, wo die devDependency ohnehin vorhanden ist.
TAURI_CLI="npx --yes @tauri-apps/cli"
if [ ! -f "$KEY_PATH" ]; then
echo "Private Key fehlt: $KEY_PATH" >&2
echo "Einmalig erzeugen mit:" >&2
echo " $TAURI_CLI signer generate -w $KEY_PATH" >&2
echo "und den public key (.pub) in src-tauri/tauri.conf.json -> plugins.updater.pubkey eintragen." >&2
exit 1
fi
VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")
PKG_VERSION=$(node -p "require('./package.json').version")
if [ "$VERSION" != "$PKG_VERSION" ]; then
echo "Version mismatch: tauri.conf.json=$VERSION package.json=$PKG_VERSION" >&2
exit 1
fi
ARCH=$(uname -m)
case "$ARCH" in
arm64|aarch64) PLATFORM_KEY="darwin-aarch64" ;;
x86_64) PLATFORM_KEY="darwin-x86_64" ;;
*) echo "Unsupported arch: $ARCH" >&2; exit 1 ;;
esac
echo "→ Build Dossier $VERSION ($PLATFORM_KEY)"
TAURI_SIGNING_PRIVATE_KEY="$(cat "$KEY_PATH")" \
TAURI_SIGNING_PRIVATE_KEY_PASSWORD="" \
$TAURI_CLI build
BUNDLE_DIR="src-tauri/target/release/bundle/macos"
TAR_GZ=$(ls "$BUNDLE_DIR"/*.app.tar.gz 2>/dev/null | head -n1 || true)
SIG_FILE="${TAR_GZ}.sig"
if [ -z "$TAR_GZ" ] || [ ! -f "$SIG_FILE" ]; then
echo "Bundle oder Signatur fehlt in $BUNDLE_DIR" >&2
exit 1
fi
ASSET_NAME=$(basename "$TAR_GZ")
ASSET_URL_NAME=$(printf '%s' "$ASSET_NAME" | sed 's/ /%20/g')
SIGNATURE=$(cat "$SIG_FILE")
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
DOWNLOAD_URL="$GITEA_REPO/releases/download/$VERSION/$ASSET_URL_NAME"
NOTES=${RELEASE_NOTES:-"Dossier $VERSION"}
cat > latest.json <<EOF
{
"version": "$VERSION",
"notes": $(node -e "process.stdout.write(JSON.stringify(process.argv[1]))" "$NOTES"),
"pub_date": "$PUB_DATE",
"platforms": {
"$PLATFORM_KEY": {
"signature": $(node -e "process.stdout.write(JSON.stringify(process.argv[1]))" "$SIGNATURE"),
"url": "$DOWNLOAD_URL"
}
}
}
EOF
echo
echo "✓ Build fertig"
echo " Bundle: $TAR_GZ"
echo " Signatur: $SIG_FILE"
echo " DMG: $(ls src-tauri/target/release/bundle/dmg/*.dmg 2>/dev/null | head -n1 || echo '(keine DMG gefunden)')"
echo " Platform: $PLATFORM_KEY"
echo " latest.json wurde im launcher-Root geschrieben."
echo
echo "Nächste Schritte:"
echo " 1) Auf Gitea Release mit Tag $VERSION erstellen und folgende Assets hochladen:"
echo " - $ASSET_NAME"
echo " - (optional) DMG für Erstinstallation"
echo " 2) latest.json committen + auf main pushen:"
echo " git add launcher/latest.json && git commit -m 'Release $VERSION' && git push origin main"
+494 -7
View File
@@ -47,6 +47,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "atk" name = "atk"
version = "0.18.2" version = "0.18.2"
@@ -124,6 +133,12 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@@ -323,6 +338,35 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "cocoa"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c"
dependencies = [
"bitflags 2.11.1",
"block",
"cocoa-foundation",
"core-foundation",
"core-graphics 0.24.0",
"foreign-types",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d"
dependencies = [
"bitflags 2.11.1",
"block",
"core-foundation",
"core-graphics-types",
"objc",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@@ -359,6 +403,19 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-graphics"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.11.1",
"core-foundation",
"core-graphics-types",
"foreign-types",
"libc",
]
[[package]] [[package]]
name = "core-graphics" name = "core-graphics"
version = "0.25.0" version = "0.25.0"
@@ -520,6 +577,17 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "2.1.1" version = "2.1.1"
@@ -656,15 +724,19 @@ dependencies = [
[[package]] [[package]]
name = "dossier-launcher" name = "dossier-launcher"
version = "0.1.0" version = "0.6.3"
dependencies = [ dependencies = [
"chrono", "chrono",
"cocoa",
"directories", "directories",
"objc",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-process",
"tauri-plugin-updater",
] ]
[[package]] [[package]]
@@ -755,6 +827,16 @@ dependencies = [
"typeid", "typeid",
] ]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.4.1" version = "2.4.1"
@@ -780,6 +862,16 @@ dependencies = [
"rustc_version", "rustc_version",
] ]
[[package]]
name = "filetime"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
"cfg-if",
"libc",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@@ -1322,6 +1414,21 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.20" version = "0.1.20"
@@ -1577,6 +1684,36 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.18",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn 2.0.117",
]
[[package]] [[package]]
name = "jni-sys" name = "jni-sys"
version = "0.3.1" version = "0.3.1"
@@ -1714,6 +1851,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.2" version = "0.8.2"
@@ -1735,6 +1878,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "markup5ever" name = "markup5ever"
version = "0.38.0" version = "0.38.0"
@@ -1767,6 +1919,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -1876,6 +2034,15 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]] [[package]]
name = "objc2" name = "objc2"
version = "0.6.4" version = "0.6.4"
@@ -2015,6 +2182,18 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.11.1",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]] [[package]]
name = "objc2-quartz-core" name = "objc2-quartz-core"
version = "0.3.2" version = "0.3.2"
@@ -2078,12 +2257,32 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]] [[package]]
name = "option-ext" name = "option-ext"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@@ -2465,15 +2664,20 @@ dependencies = [
"http-body", "http-body",
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
"log", "log",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde", "serde",
"serde_json", "serde_json",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http",
@@ -2509,6 +2713,20 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.2" version = "2.1.2"
@@ -2524,6 +2742,92 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.11.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation",
"core-foundation-sys",
"jni 0.22.4",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.22"
@@ -2539,6 +2843,15 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "schannel"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.8.22" version = "0.8.22"
@@ -2596,6 +2909,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.11.1",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "selectors" name = "selectors"
version = "0.36.1" version = "0.36.1"
@@ -2806,6 +3142,22 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd_cesu8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.3" version = "1.0.3"
@@ -2918,6 +3270,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "swift-rs" name = "swift-rs"
version = "1.0.7" version = "1.0.7"
@@ -2992,7 +3350,7 @@ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2",
"core-foundation", "core-foundation",
"core-graphics", "core-graphics 0.25.0",
"crossbeam-channel", "crossbeam-channel",
"dbus", "dbus",
"dispatch2", "dispatch2",
@@ -3001,7 +3359,7 @@ dependencies = [
"gdkwayland-sys", "gdkwayland-sys",
"gdkx11-sys", "gdkx11-sys",
"gtk", "gtk",
"jni", "jni 0.21.1",
"libc", "libc",
"log", "log",
"ndk", "ndk",
@@ -3034,6 +3392,17 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "tar"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.16" version = "0.12.16"
@@ -3057,7 +3426,7 @@ dependencies = [
"gtk", "gtk",
"heck 0.5.0", "heck 0.5.0",
"http", "http",
"jni", "jni 0.21.1",
"libc", "libc",
"log", "log",
"mime", "mime",
@@ -3211,6 +3580,49 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "tauri-plugin-process"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.11.1" version = "2.11.1"
@@ -3221,7 +3633,7 @@ dependencies = [
"dpi", "dpi",
"gtk", "gtk",
"http", "http",
"jni", "jni 0.21.1",
"objc2", "objc2",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit", "objc2-web-kit",
@@ -3244,7 +3656,7 @@ checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
"jni", "jni 0.21.1",
"log", "log",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
@@ -3311,6 +3723,19 @@ dependencies = [
"toml 1.1.2+spec-1.1.0", "toml 1.1.2+spec-1.1.0",
] ]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "tendril" name = "tendril"
version = "0.5.0" version = "0.5.0"
@@ -3431,6 +3856,16 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.18" version = "0.7.18"
@@ -3727,6 +4162,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@@ -4019,6 +4460,15 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "webpki-root-certs"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webview2-com" name = "webview2-com"
version = "0.38.2" version = "0.38.2"
@@ -4258,6 +4708,15 @@ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
@@ -4698,7 +5157,7 @@ dependencies = [
"gtk", "gtk",
"http", "http",
"javascriptcore-rs", "javascriptcore-rs",
"jni", "jni 0.21.1",
"libc", "libc",
"ndk", "ndk",
"objc2", "objc2",
@@ -4745,6 +5204,16 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.2" version = "0.8.2"
@@ -4789,6 +5258,12 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.4" version = "0.2.4"
@@ -4822,6 +5297,18 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.0",
"memchr",
]
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.21" version = "1.0.21"
+11 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "dossier-launcher" name = "dossier-launcher"
version = "0.1.0" version = "0.6.3"
description = "Dossier — Projekt-Launcher fuer Rhino" description = "Dossier — Projekt-Launcher fuer Rhino"
authors = ["Karim Gabriele Varano"] authors = ["Karim Gabriele Varano"]
edition = "2021" edition = "2021"
@@ -13,13 +13,22 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
directories = "5" directories = "5"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
# macOS-only: native NSWindow-Calls fuer abgerundete Splash-Ecken.
# Cocoa + objc sind die etablierten low-level Bindings; Tauris transparent:true
# Window haette sonst weisse Ecken weil WkWebView per default opaque ist.
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.26"
objc = "0.2"
[profile.release] [profile.release]
panic = "abort" panic = "abort"
codegen-units = 1 codegen-units = 1
+6 -3
View File
@@ -1,10 +1,13 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability fuer das Hauptfenster", "description": "Capability fuer Haupt- und Splash-Fenster",
"windows": ["main"], "windows": ["main", "splash"],
"permissions": [ "permissions": [
"core:default", "core:default",
"dialog:default" "core:webview:allow-print",
"dialog:default",
"updater:default",
"process:allow-restart"
] ]
} }
+869 -14
View File
@@ -1,10 +1,18 @@
// Verhindert ein extra Konsolen-Fenster auf Windows im Release-Build. // Verhindert ein extra Konsolen-Fenster auf Windows im Release-Build.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Emitter, Manager,
};
// Statisches Modul-Manifest — in die Binary einkompiliert, sodass die App // Statisches Modul-Manifest — in die Binary einkompiliert, sodass die App
// keine externe Datei zur Laufzeit braucht. Wer Module aendert: modules.json // keine externe Datei zur Laufzeit braucht. Wer Module aendert: modules.json
@@ -18,6 +26,47 @@ struct Project {
modules: Vec<String>, modules: Vec<String>,
#[serde(rename = "lastOpened")] #[serde(rename = "lastOpened")]
last_opened: Option<String>, last_opened: Option<String>,
#[serde(rename = "createdAt", default, skip_serializing_if = "Option::is_none")]
created_at: Option<String>,
#[serde(rename = "windowLayout", default, skip_serializing_if = "Option::is_none")]
window_layout: Option<String>,
#[serde(default)]
pinned: bool,
#[serde(rename = "tagIds", default)]
tag_ids: Vec<String>,
// Geschwister-Dateien im selben Projekt-Ordner. Hauptdatei bleibt `path`;
// diese hier sind zusaetzliche .3dm/PDF/sonstwas die zum Projekt gehoeren.
#[serde(rename = "extraFiles", default)]
extra_files: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct Tag {
id: String,
name: String,
color: String, // hex incl. "#"
}
// Layer-Template — eine Sublayer-Definition fuer das Default-Schema
// (00_RASTER, 01_VERMESSUNG, …). Wird vom Plugin beim FIRST_RUN an die
// Ebenen-React-UI gesendet, ueberschreibt deren hardcoded INITIAL_EBENEN.
#[derive(Serialize, Deserialize, Clone, Debug)]
struct LayerTemplate {
code: String,
name: String,
color: String,
lw: f64,
}
// Viewport-Color-Preset — vollstaendiger Color-Set unter einem Namen.
// Built-ins ("Rhino-Standard", "Dossier-Standard") sind im Frontend hardcoded;
// custom Presets landen hier in den Settings, koennen exportiert/importiert
// werden.
#[derive(Serialize, Deserialize, Clone, Debug)]
struct ColorPreset {
id: String,
name: String,
colors: ViewportColors,
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
@@ -30,21 +79,115 @@ struct ProjectConfig {
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
struct Settings { struct Settings {
// App-Name oder absoluter Pfad zur Rhino-App. `open -a` akzeptiert beides:
// "Rhinoceros 8" -> sucht in /Applications
// "/Applications/Rhino 8.app" oder ein Beta-Build irgendwo anders.
#[serde(rename = "rhinoApp")] #[serde(rename = "rhinoApp")]
rhino_app: String, rhino_app: String,
#[serde(rename = "templatePath", default)]
template_path: Option<String>,
#[serde(default)]
tags: Vec<Tag>,
#[serde(rename = "viewColorPresets", default)]
view_color_presets: Vec<ColorPreset>,
// Auto-Load: Plugin nach Rhino-Start via AppleScript triggern.
// Default false — User aktiviert bewusst in Settings.
#[serde(rename = "autoLoadPlugin", default)]
auto_load_plugin: bool,
// Pfad zur startup.py die geladen werden soll. Default: das DOSSIER-Repo
// wo der Launcher steckt (siehe default_plugin_startup_path()).
#[serde(rename = "pluginStartupPath", default)]
plugin_startup_path: Option<String>,
}
fn default_plugin_startup_path() -> String {
// Im installierten App-Bundle liegt startup.py unter
// /Applications/Dossier.app/Contents/Resources/rhino/startup.py
// (Tauri kopiert via bundle.resources in tauri.conf.json dorthin).
// Damit laeuft DOSSIER fuer neue User out-of-the-box, ohne dass sie den
// Pfad in den Settings setzen muessen. Dev-Fallback: Repo-Pfad.
if let Ok(exe) = std::env::current_exe() {
if let Some(contents_dir) = exe.parent().and_then(|p| p.parent()) {
let bundled = contents_dir.join("Resources/rhino/startup.py");
if bundled.is_file() {
return bundled.to_string_lossy().into_owned();
}
}
}
"/Users/karim/STUDIO/DOSSIER/rhino/startup.py".to_string()
}
#[tauri::command]
fn get_default_plugin_startup_path() -> String {
default_plugin_startup_path()
} }
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Self { Self {
rhino_app: "Rhinoceros 8".into(), rhino_app: "Rhinoceros 8".into(),
template_path: None,
tags: Vec::new(),
view_color_presets: Vec::new(),
// Default ON — mit korrektem Shebang in startup.py ist Auto-Load
// silent (kein ScriptEditor) und lohnt sich daher als Default.
auto_load_plugin: true,
plugin_startup_path: None,
} }
} }
} }
// Dossier-Settings: WIRD VON RHINO GELESEN. Pfad ist bewusst projekt-stabil
// (~/Library/Application Support/Dossier/dossier_settings.json), damit
// oberleiste.py sie ohne Tauri-Abhaengigkeit findet.
// Viewport-Colors als Hex-Strings ("#RRGGBB"). Wenn None: Rhino-Default
// behalten (keine Aenderung).
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
struct ViewportColors {
#[serde(default, skip_serializing_if = "Option::is_none")] background: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] grid_line: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] grid_major: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] grid_x: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] grid_y: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] world_x: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] world_y: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] world_z: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
struct DossierSettings {
#[serde(rename = "windowLayout", default)]
window_layout: Option<String>,
#[serde(rename = "autoApplyLayout", default)]
auto_apply_layout: bool,
#[serde(rename = "pendingApplyLayout", default, skip_serializing_if = "Option::is_none")]
pending_apply_layout: Option<String>,
// Viewport-Colors (App-weit, nicht per Document). Plugin liest + applied
// via Rhino.ApplicationSettings.AppearanceSettings.
#[serde(rename = "viewportColors", default)]
viewport_colors: ViewportColors,
#[serde(rename = "autoApplyViewColors", default)]
auto_apply_view_colors: bool,
#[serde(rename = "pendingApplyViewColors", default)]
pending_apply_view_colors: bool,
// Display-Mode-Import: Liste von .ini-Pfaden, die Plugin nacheinander
// via Rhino.Display.DisplayModeDescription.ImportFromFile importiert
// und dann clearet.
#[serde(rename = "pendingImportDisplayModes", default)]
pending_import_display_modes: Vec<String>,
// Default-Sublayer-Schema fuer neue Projekte. Plugin (rhinopanel.py) liest
// das beim FIRST_RUN und sendet's als defaultEbenen an die Ebenen-React.
// Leere Liste = React faellt auf hardcoded INITIAL_EBENEN zurueck.
#[serde(rename = "layerSchema", default)]
layer_schema: Vec<LayerTemplate>,
// Wenn true: oberleiste-Plugin liest doc.Strings["dossier_ebenen"] und
// schreibt das in layerSchema (ueberschreibt!) + cleart das Flag.
// Erlaubt dem User "Aktuelles Rhino-Setup als Default speichern".
#[serde(rename = "pendingExportEbenen", default)]
pending_export_ebenen: bool,
}
fn dossier_dir() -> PathBuf { fn dossier_dir() -> PathBuf {
// ~/Library/Application Support/Dossier auf macOS, // ~/Library/Application Support/Dossier auf macOS,
// entsprechend Plattform-Pendants sonst. // entsprechend Plattform-Pendants sonst.
@@ -63,6 +206,10 @@ fn settings_path() -> PathBuf {
dossier_dir().join("settings.json") dossier_dir().join("settings.json")
} }
fn dossier_settings_path() -> PathBuf {
dossier_dir().join("dossier_settings.json")
}
fn load_settings() -> Settings { fn load_settings() -> Settings {
let p = settings_path(); let p = settings_path();
if !p.exists() { if !p.exists() {
@@ -74,8 +221,18 @@ fn load_settings() -> Settings {
.unwrap_or_default() .unwrap_or_default()
} }
#[tauri::command] fn load_dossier_settings() -> DossierSettings {
fn list_recent() -> Vec<Project> { let p = dossier_settings_path();
if !p.exists() {
return DossierSettings::default();
}
fs::read_to_string(&p)
.ok()
.and_then(|raw| serde_json::from_str(&raw).ok())
.unwrap_or_default()
}
fn list_recent_internal() -> Vec<Project> {
let p = recent_path(); let p = recent_path();
if !p.exists() { if !p.exists() {
return vec![]; return vec![];
@@ -84,6 +241,11 @@ fn list_recent() -> Vec<Project> {
serde_json::from_str(&raw).unwrap_or_default() serde_json::from_str(&raw).unwrap_or_default()
} }
#[tauri::command]
fn list_recent() -> Vec<Project> {
list_recent_internal()
}
#[tauri::command] #[tauri::command]
fn save_recent(projects: Vec<Project>) -> Result<(), String> { fn save_recent(projects: Vec<Project>) -> Result<(), String> {
let p = recent_path(); let p = recent_path();
@@ -124,18 +286,349 @@ fn read_project_config(path3dm: String) -> Result<Option<ProjectConfig>, String>
Ok(Some(cfg)) Ok(Some(cfg))
} }
#[tauri::command] fn plugin_loaded_marker_path() -> PathBuf {
fn open_rhino(path3dm: String) -> Result<(), String> { // startup.py schreibt diese Datei am Ende von `_load_all` (nach Panel-
// macOS: `open -a <app> <file>`. `<app>` kann ein App-Name (in /Applications // Registrierung). Der Launcher pollt darauf und schliesst den Splash.
// gesucht) oder ein absoluter Pfad zur .app sein. Aus den Settings — Default dossier_dir().join("plugin_loaded.flag")
// "Rhinoceros 8", falls der User nichts angepasst hat. }
fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), String> {
let settings = load_settings(); let settings = load_settings();
// XML-Edit nur sinnvoll wenn Rhino nicht laeuft (sonst ueberschreibt's
// beim Beenden) UND der Eintrag fuer den naechsten Start eh schon greift.
if settings.auto_load_plugin && !is_rhino_running() {
let startup_path = settings.plugin_startup_path
.clone()
.unwrap_or_else(default_plugin_startup_path);
if Path::new(&startup_path).is_file() {
if let Err(e) = ensure_rhino_startup_command(&startup_path) {
eprintln!("auto-load plugin: {e}");
}
} else {
eprintln!("auto-load plugin: Startup-Pfad nicht gefunden: {startup_path}");
}
}
// Splash NUR zeigen wenn Auto-Load aktiv (sonst gibt's nichts zu warten).
let show_splash = settings.auto_load_plugin;
let marker = plugin_loaded_marker_path();
if show_splash {
let _ = fs::remove_file(&marker);
if let Some(splash) = app.get_webview_window("splash") {
let _ = splash.show();
}
}
Command::new("open") Command::new("open")
.args(["-a", &settings.rhino_app, &path3dm]) .args(["-a", &settings.rhino_app, path3dm])
.spawn() .spawn()
.map_err(|e| format!( .map_err(|e| format!(
"open-Befehl fehlgeschlagen ({}): {e}", settings.rhino_app "open-Befehl fehlgeschlagen ({}): {e}", settings.rhino_app
))?; ))?;
if show_splash {
let app_clone = app.clone();
std::thread::spawn(move || {
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(90);
loop {
if marker.is_file() {
break;
}
if start.elapsed() > timeout {
eprintln!("splash: timeout — Plugin hat sich nicht zurueckgemeldet");
break;
}
std::thread::sleep(std::time::Duration::from_millis(250));
}
let _ = fs::remove_file(&marker);
if let Some(splash) = app_clone.get_webview_window("splash") {
let _ = splash.hide();
}
});
}
Ok(())
}
fn rhino_settings_xml_path() -> PathBuf {
let home = std::env::var("HOME").map(PathBuf::from).unwrap_or_default();
home.join("Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml")
}
// Traegt `_RunPythonScript <pfad>` in Rhinos „Run these commands every time a
// model is opened" Liste ein (XML-Setting `Options/General/StartupCommands`).
// Idempotent: ist der exakte Command schon drin, passiert nichts. Existieren
// andere Commands, wird unserer per Newline angehaengt.
//
// Warum dieser Weg statt AppleScript-Keystrokes: Rhino's native Startup-
// Command-Liste ist persistent, layout-unabhaengig und braucht keine
// Accessibility-Permission. Einmal gesetzt, laeuft das Plugin bei JEDEM
// Rhino-Start — egal ob via Launcher oder direkt.
//
// Sicherheit: schreibt nur wenn Rhino NICHT laeuft (sonst ueberschreibt Rhino
// beim Beenden unsere Aenderung). XML wird per simpler String-Manipulation
// editiert, NICHT geparst + reserialisiert, um Rhinos Format 1:1 zu erhalten.
fn ensure_rhino_startup_command(startup_path: &str) -> Result<(), String> {
let xml_path = rhino_settings_xml_path();
if !xml_path.is_file() {
return Err(format!(
"Rhino-Settings-Datei nicht gefunden — Rhino mind. einmal starten + beenden: {}",
xml_path.display()
));
}
if is_rhino_running() {
return Err(
"Rhino laeuft — bitte erst beenden, sonst ueberschreibt Rhino die \
Aenderung beim Schliessen."
.into(),
);
}
// Dash + Quotes ist die einzige Form die Rhinos StartupCommands-Feld OHNE
// File-Dialog ausfuehrt (verifiziert 2026-05-17 Mac Rhino 8). Die no-dash-
// Form aus der interaktiven Command-Line oeffnet hier den Dialog. Trotz
// des Dashes laesst Mac Rhino 8 in diesem Kontext den Shebang `#! python 3`
// gelten und startet den CPython-3-Engine korrekt.
let cmd = format!(r#"_-RunPythonScript "{startup_path}""#);
let content = fs::read_to_string(&xml_path)
.map_err(|e| format!("Settings lesen: {e}"))?;
let entry_open = r#"<entry key="StartupCommands">"#;
let entry_close = "</entry>";
let new_content = if let Some(start) = content.find(entry_open) {
// Case A: Eintrag existiert bereits — anhaengen falls noch nicht drin.
let value_start = start + entry_open.len();
let value_end_rel = content[value_start..]
.find(entry_close)
.ok_or_else(|| "StartupCommands-Entry ohne schliessendes </entry>".to_string())?;
let current = &content[value_start..value_start + value_end_rel];
if current.lines().any(|l| l.trim() == cmd) {
return Ok(()); // schon drin, idempotent
}
let new_value = if current.trim().is_empty() {
cmd.clone()
} else {
format!("{current}\n{cmd}")
};
format!(
"{}{new_value}{}",
&content[..value_start],
&content[value_start + value_end_rel..]
)
} else if let Some(general_pos) = content.find(r#"<child key="General">"#) {
// Case B: General-Subtree existiert, StartupCommands noch nicht — Entry rein.
let insert_at = general_pos + r#"<child key="General">"#.len();
let insertion = format!(
"\n <entry key=\"StartupCommands\">{cmd}</entry>"
);
format!(
"{}{insertion}{}",
&content[..insert_at],
&content[insert_at..]
)
} else if let Some(options_pos) = content.find(r#"<child key="Options">"#) {
// Case C: Weder StartupCommands noch General-Subtree vorhanden —
// ganzen Subtree neu anlegen. Rhino cleant den General-Subtree wenn er
// leer ist, daher landen wir hier nach jedem "Eintrag im UI geleert".
let insert_at = options_pos + r#"<child key="Options">"#.len();
let insertion = format!(
"\n <child key=\"General\">\n <entry key=\"StartupCommands\">{cmd}</entry>\n </child>"
);
format!(
"{}{insertion}{}",
&content[..insert_at],
&content[insert_at..]
)
} else {
return Err(
"Konnte Eintrag nicht setzen — Rhino-Settings haben unerwartetes Format \
(Options-Subtree fehlt)."
.into(),
);
};
fs::write(&xml_path, new_content)
.map_err(|e| format!("Settings schreiben: {e}"))?;
Ok(())
}
#[tauri::command]
fn open_rhino(app: tauri::AppHandle, path3dm: String) -> Result<(), String> {
open_rhino_internal(&app, &path3dm)
}
#[tauri::command]
fn trigger_plugin_load_now() -> Result<(), String> {
// Schreibt den `_RunPythonScript <pfad>` Eintrag in Rhinos Startup-Command-
// Liste. Greift beim naechsten Rhino-Start automatisch. Idempotent.
let settings = load_settings();
let startup_path = settings.plugin_startup_path
.unwrap_or_else(default_plugin_startup_path);
if !Path::new(&startup_path).is_file() {
return Err(format!("Startup-Pfad nicht gefunden: {startup_path}"));
}
ensure_rhino_startup_command(&startup_path)
}
#[derive(Serialize, Clone, Debug, Default)]
struct FileMeta {
exists: bool,
size: u64,
#[serde(rename = "modifiedIso")]
modified_iso: Option<String>,
}
// Liefert das Projekt-Thumbnail als Base64-Data-URL falls vorhanden.
// Erwarteter Pfad: `<path>.thumb.png` (z.B. /foo/bar.3dm -> /foo/bar.3dm.thumb.png).
// Base64 statt asset://-Protokoll weil's keine zusaetzliche Tauri-Config braucht
// und Thumbnails klein genug sind (typisch 5-20 KB).
#[tauri::command]
fn read_thumbnail(path: String) -> Option<serde_json::Value> {
use std::io::Read;
let thumb_path = format!("{}.thumb.png", path);
let p = Path::new(&thumb_path);
if !p.is_file() { return None; }
let mut buf = Vec::new();
if fs::File::open(p).and_then(|mut f| f.read_to_end(&mut buf)).is_err() {
return None;
}
// Manuelles Base64-Encoding — keine extra Crate noetig
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut encoded = String::with_capacity((buf.len() + 2) / 3 * 4);
for chunk in buf.chunks(3) {
let b0 = chunk[0];
let b1 = if chunk.len() > 1 { chunk[1] } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] } else { 0 };
encoded.push(ALPHABET[(b0 >> 2) as usize] as char);
encoded.push(ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
if chunk.len() > 1 {
encoded.push(ALPHABET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
} else { encoded.push('='); }
if chunk.len() > 2 {
encoded.push(ALPHABET[(b2 & 0x3f) as usize] as char);
} else { encoded.push('='); }
}
let mtime = fs::metadata(p).ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64)
.and_then(|s| chrono::DateTime::<chrono::Utc>::from_timestamp(s, 0))
.map(|d| d.to_rfc3339());
Some(serde_json::json!({
"dataUrl": format!("data:image/png;base64,{}", encoded),
"modifiedIso": mtime,
}))
}
#[tauri::command]
fn get_file_meta(path: String) -> FileMeta {
let p = Path::new(&path);
let meta = match fs::metadata(p) {
Ok(m) => m,
Err(_) => return FileMeta::default(),
};
let size = meta.len();
let modified_iso = meta.modified().ok().and_then(|t| {
let secs = t.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs() as i64;
chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0).map(|d| d.to_rfc3339())
});
FileMeta { exists: true, size, modified_iso }
}
#[tauri::command]
fn write_text_file(path: String, content: String) -> Result<(), String> {
let p = Path::new(&path);
if let Some(parent) = p.parent() {
fs::create_dir_all(parent).map_err(|e| format!("Parent-Ordner: {e}"))?;
}
fs::write(p, content).map_err(|e| format!("Schreiben fehlgeschlagen: {e}"))
}
#[tauri::command]
fn read_text_file(path: String) -> Result<String, String> {
fs::read_to_string(Path::new(&path))
.map_err(|e| format!("Lesen fehlgeschlagen: {e}"))
}
#[tauri::command]
fn create_snapshot(path: String) -> Result<String, String> {
// Legt einen Zeit-stempel-Snapshot der .3dm in <projectdir>/_snapshots/ an.
// Default-Lebenswerk-Schutz: schnell, unbeobachtet, ohne Rhino-Restart.
let src = Path::new(&path);
if !src.is_file() {
return Err(format!("Quelle nicht gefunden: {path}"));
}
let parent = src.parent()
.ok_or_else(|| "Pfad hat keinen Parent-Ordner".to_string())?;
let snap_dir = parent.join("_snapshots");
fs::create_dir_all(&snap_dir).map_err(|e| format!("_snapshots/ anlegen: {e}"))?;
let stem = src.file_stem().and_then(|s| s.to_str()).unwrap_or("snapshot");
let ext = src.extension().and_then(|s| s.to_str()).unwrap_or("3dm");
let ts = chrono::Local::now().format("%Y-%m-%d_%H%M%S");
let target = snap_dir.join(format!("{}_{}.{}", stem, ts, ext));
fs::copy(src, &target).map_err(|e| format!("Snapshot fehlgeschlagen: {e}"))?;
Ok(target.to_string_lossy().to_string())
}
#[tauri::command]
fn show_in_finder(path: String) -> Result<(), String> {
// -R offenbart die Datei im Finder (Parent-Ordner wird geoeffnet, Datei
// selektiert). Funktioniert auch wenn der Pfad nicht existiert.
Command::new("open")
.args(["-R", &path])
.spawn()
.map_err(|e| format!("Finder-Reveal fehlgeschlagen: {e}"))?;
Ok(())
}
#[tauri::command]
fn is_project_open(path: String) -> bool {
// Mac Rhino legt einen Lock neben der .3dm an: `<basename>.3dm.rhl`.
// Existenz dieses Files = die .3dm ist gerade geoeffnet.
let lock_path = format!("{}.rhl", path);
Path::new(&lock_path).is_file()
}
#[tauri::command]
fn is_rhino_running() -> bool {
// pgrep -x matcht exakte Process-Names. Mac Rhinos Prozess heisst
// "Rhinoceros" (Bundle-Identifier in App-Bundle). Wir akzeptieren beide
// 8er und ggf. 9er.
Command::new("pgrep")
.args(["-x", "Rhinoceros"])
.output()
.map(|out| out.status.success())
.unwrap_or(false)
}
#[tauri::command]
fn copy_template_to(target_path: String) -> Result<(), String> {
// Kopiert die in den Settings eingestellte Template-3dm nach target_path.
// Wenn keine Template gesetzt ist oder die Quelle nicht existiert, wird
// einfach eine leere .3dm angelegt — der User bekommt zumindest eine
// Datei, die Rhino oeffnen kann. (Eine echte leere Rhino-3dm waere ein
// Bytes-Blob; wir lassen das Erstellen Rhino ueberlassen, indem wir keine
// Datei anlegen und Rhino mit -newdoc starten waere... nein, wir kopieren
// nur wenn Template existiert. Wenn nicht, gibt's einen Hinweis-Fehler.)
let settings = load_settings();
let tpl = settings.template_path
.as_ref()
.ok_or_else(|| "Keine Template-Datei in den Einstellungen gesetzt".to_string())?;
let src = Path::new(tpl);
if !src.is_file() {
return Err(format!("Template-Datei nicht gefunden: {tpl}"));
}
let dst = Path::new(&target_path);
if dst.exists() {
return Err(format!("Zieldatei existiert bereits: {target_path}"));
}
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent).map_err(|e| format!("Zielordner anlegen: {e}"))?;
}
fs::copy(src, dst).map_err(|e| format!("Kopieren fehlgeschlagen: {e}"))?;
Ok(()) Ok(())
} }
@@ -155,20 +648,382 @@ fn read_modules_manifest() -> Result<serde_json::Value, String> {
serde_json::from_str(MODULES_JSON).map_err(|e| e.to_string()) serde_json::from_str(MODULES_JSON).map_err(|e| e.to_string())
} }
// --- Window-Layouts (Rhino) ------------------------------------------------
// Mac Rhino 8 speichert Layouts als XML in
// ~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces/<GUID>.xml
// Der Dateiname ist eine GUID, der Display-Name steht im name="..." Attribut
// des Root-<RhinoUI>-Elements. Windows Rhino nutzt .rwl-Dateien — wir behalten
// den alten Pfad als Fallback fuer den Fall, dass das Format aendert.
fn extract_layout_name_from_xml(content: &str) -> Option<String> {
// 1) `<RhinoUI ... name="XYZ">` (Mac-Pfad)
if let Some(start) = content.find("<RhinoUI") {
let after = &content[start..];
if let Some(end_tag) = after.find('>') {
let header = &after[..end_tag];
if let Some(npos) = header.find("name=\"") {
let rest = &header[npos + 6..];
if let Some(qend) = rest.find('"') {
let name = rest[..qend].trim();
if !name.is_empty() {
return Some(name.to_string());
}
}
}
}
}
// 2) `<locale_1033>XYZ</locale_1033>` als Fallback
if let Some(start) = content.find("<locale_1033>") {
let rest = &content[start + "<locale_1033>".len()..];
if let Some(end) = rest.find("</locale_1033>") {
let name = rest[..end].trim();
if !name.is_empty() {
return Some(name.to_string());
}
}
}
None
}
#[tauri::command]
fn list_window_layouts() -> Vec<String> {
let home = directories::UserDirs::new()
.map(|d| d.home_dir().to_path_buf())
.unwrap_or_else(|| PathBuf::from("~"));
let mut out: Vec<String> = Vec::new();
// Mac Rhino 8: workspaces/<GUID>.xml
let workspaces = home.join(
"Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces",
);
if workspaces.is_dir() {
if let Ok(entries) = fs::read_dir(&workspaces) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().and_then(|s| s.to_str()).map(|s| s.eq_ignore_ascii_case("xml"))
.unwrap_or(false)
{
if let Ok(content) = fs::read_to_string(&p) {
if let Some(name) = extract_layout_name_from_xml(&content) {
if !out.contains(&name) {
out.push(name);
}
}
}
}
}
}
}
// Legacy/Windows-Fallback: .rwl-Dateien
let legacy_dirs = [
"Library/Application Support/McNeel/Rhinoceros/8.0/UI/MainWindowLayouts",
"Library/Application Support/McNeel/Rhinoceros/8.0/MainWindowLayouts",
"Library/Application Support/McNeel/Rhinoceros/8.0/UI/Layouts",
];
for rel in legacy_dirs.iter() {
let dir = home.join(rel);
if !dir.is_dir() {
continue;
}
if let Ok(entries) = fs::read_dir(&dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().and_then(|s| s.to_str()).map(|s| s.eq_ignore_ascii_case("rwl"))
.unwrap_or(false)
{
if let Some(stem) = p.file_stem().and_then(|s| s.to_str()) {
let name = stem.to_string();
if !out.contains(&name) {
out.push(name);
}
}
}
}
}
}
out.sort();
out
}
#[tauri::command]
fn read_dossier_settings() -> DossierSettings {
load_dossier_settings()
}
#[tauri::command]
fn save_dossier_settings(settings: DossierSettings) -> Result<(), String> {
let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
fs::write(dossier_settings_path(), raw)
.map_err(|e| format!("dossier_settings.json schreiben: {e}"))
}
#[tauri::command]
fn request_ebenen_export() -> Result<(), String> {
// Setzt das Flag — Plugin pickt's beim naechsten Idle auf, liest
// doc.Strings["dossier_ebenen"] und schreibt's in layerSchema.
let mut cfg = load_dossier_settings();
cfg.pending_export_ebenen = true;
let raw = serde_json::to_string_pretty(&cfg).map_err(|e| e.to_string())?;
fs::write(dossier_settings_path(), raw)
.map_err(|e| format!("dossier_settings.json schreiben: {e}"))
}
#[tauri::command]
fn apply_view_colors_now() -> Result<(), String> {
// Flag setzen — Plugin pickt's im Idle auf und appliert die aktuell
// gespeicherten Colors via Rhino API.
let mut cfg = load_dossier_settings();
cfg.pending_apply_view_colors = true;
let raw = serde_json::to_string_pretty(&cfg).map_err(|e| e.to_string())?;
fs::write(dossier_settings_path(), raw)
.map_err(|e| format!("dossier_settings.json schreiben: {e}"))
}
#[tauri::command]
fn queue_import_display_mode(path: String) -> Result<(), String> {
// Haengt einen .ini-Pfad an pendingImportDisplayModes an. Plugin importiert
// beim naechsten Idle, leert dann die Liste.
if !Path::new(&path).is_file() {
return Err(format!("Datei nicht gefunden: {path}"));
}
let mut cfg = load_dossier_settings();
cfg.pending_import_display_modes.push(path);
let raw = serde_json::to_string_pretty(&cfg).map_err(|e| e.to_string())?;
fs::write(dossier_settings_path(), raw)
.map_err(|e| format!("dossier_settings.json schreiben: {e}"))
}
#[tauri::command]
fn apply_layout_now(name: String) -> Result<(), String> {
// Setzt das pendingApplyLayout-Flag, behaelt sonst alle Settings bei.
// oberleiste.py's Idle-Handler pollt den Wert, fuehrt das Layout aus und
// loescht das Flag. So funktioniert das "Jetzt anwenden" auch bei
// bereits laufender Rhino-Instanz.
let mut cfg = load_dossier_settings();
cfg.pending_apply_layout = Some(name);
let raw = serde_json::to_string_pretty(&cfg).map_err(|e| e.to_string())?;
fs::write(dossier_settings_path(), raw)
.map_err(|e| format!("dossier_settings.json schreiben: {e}"))
}
// --- Window / Tray ---------------------------------------------------------
fn show_main_window(app: &tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
// Tray-Menu mit den letzten 5 Projekten + Standard-Eintraegen. Wird beim
// Startup gebaut und bei refresh_tray_menu (nach save_recent) neu erstellt.
fn build_tray_menu(app: &tauri::AppHandle) -> tauri::Result<tauri::menu::Menu<tauri::Wry>> {
let show = MenuItem::with_id(app, "show", "Dossier öffnen", true, None::<&str>)?;
let sep1 = PredefinedMenuItem::separator(app)?;
let new_proj = MenuItem::with_id(app, "nav:new", "Neues Projekt …", true, None::<&str>)?;
let settings = MenuItem::with_id(app, "nav:settings", "Einstellungen", true, None::<&str>)?;
let check_update = MenuItem::with_id(app, "nav:check-update", "Nach Updates suchen", true, None::<&str>)?;
let sep2 = PredefinedMenuItem::separator(app)?;
let quit = MenuItem::with_id(app, "quit", "Beenden", true, Some("Cmd+Q"))?;
let recent = list_recent_internal();
let mut recent_items: Vec<MenuItem<tauri::Wry>> = Vec::new();
for p in recent.iter().take(5) {
// ID = "open:<path>" — Path kann Leerzeichen/Slashes haben, das macht
// Tauri Menu-IDs nicht aus. Wir extrahieren den Pfad spaeter zurueck.
let id = format!("open:{}", p.path);
let label = format!("Öffnen — {}", p.name);
recent_items.push(MenuItem::with_id(app, &id, &label, true, None::<&str>)?);
}
let mut items: Vec<&dyn tauri::menu::IsMenuItem<tauri::Wry>> =
vec![&show, &sep1, &new_proj, &settings, &check_update];
let sep_recent;
if !recent_items.is_empty() {
sep_recent = PredefinedMenuItem::separator(app)?;
items.push(&sep_recent);
for item in &recent_items {
items.push(item);
}
}
items.push(&sep2);
items.push(&quit);
Menu::with_items(app, &items)
}
#[tauri::command]
fn refresh_tray_menu(app: tauri::AppHandle) -> Result<(), String> {
let menu = build_tray_menu(&app).map_err(|e| e.to_string())?;
if let Some(tray) = app.tray_by_id("main") {
tray.set_menu(Some(menu)).map_err(|e| e.to_string())?;
}
Ok(())
}
#[cfg(target_os = "macos")]
fn apply_macos_rounded_corners(window: &tauri::WebviewWindow, radius: f64) {
use cocoa::appkit::NSWindow;
use cocoa::base::{id, NO, YES};
use objc::{class, msg_send, sel, sel_impl};
let ns_window_ptr = match window.ns_window() {
Ok(p) => p as id,
Err(_) => return,
};
unsafe {
// NSWindow: opaque off + clearColor background — sodass das contentView
// (mit dem clipping layer) wirklich sichtbar wird, statt von einem
// weissen Window-Background ueberdeckt zu werden.
let _: () = msg_send![ns_window_ptr, setOpaque: NO];
let clear: id = msg_send![class!(NSColor), clearColor];
ns_window_ptr.setBackgroundColor_(clear);
// contentView: wantsLayer + Layer mit cornerRadius — clipt alle
// Subviews (inkl. WkWebView) auf das abgerundete Rechteck.
let content_view: id = msg_send![ns_window_ptr, contentView];
let _: () = msg_send![content_view, setWantsLayer: YES];
let layer: id = msg_send![content_view, layer];
let _: () = msg_send![layer, setCornerRadius: radius];
let _: () = msg_send![layer, setMasksToBounds: YES];
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let is_quitting = Arc::new(AtomicBool::new(false));
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
list_recent, list_recent,
save_recent, save_recent,
write_project_config, write_project_config,
read_project_config, read_project_config,
open_rhino, open_rhino,
trigger_plugin_load_now,
get_default_plugin_startup_path,
show_in_finder,
is_rhino_running,
is_project_open,
copy_template_to,
get_file_meta,
read_thumbnail,
create_snapshot,
write_text_file,
read_text_file,
read_modules_manifest, read_modules_manifest,
read_settings, read_settings,
save_settings save_settings,
list_window_layouts,
read_dossier_settings,
save_dossier_settings,
apply_layout_now,
apply_view_colors_now,
queue_import_display_mode,
request_ebenen_export,
refresh_tray_menu,
]) ])
.run(tauri::generate_context!()) .on_window_event({
.expect("Fehler beim Starten der Tauri-App"); let is_quitting = is_quitting.clone();
move |window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if !is_quitting.load(Ordering::SeqCst) {
// Fenster schliessen versteckt — App bleibt im Tray
// verfuegbar, bis explizit "Beenden" gewaehlt wird.
api.prevent_close();
let _ = window.hide();
}
}
}
})
.setup({
let is_quitting = is_quitting.clone();
move |app| {
// Splash-Window: macOS NSWindow transparent + Layer-cornerRadius
// setzen. Tauris `transparent: true` allein gibt weisse Ecken,
// weil WkWebView per default opaque rendert. Wir machen NSWindow
// explizit non-opaque + setzen contentView.layer.cornerRadius +
// masksToBounds — das clipt den gesamten Inhalt rund.
#[cfg(target_os = "macos")]
if let Some(splash) = app.get_webview_window("splash") {
apply_macos_rounded_corners(&splash, 16.0);
}
let menu = build_tray_menu(app.handle())?;
let is_quitting_menu = is_quitting.clone();
let _tray = TrayIconBuilder::with_id("main")
.icon(app.default_window_icon().unwrap().clone())
.icon_as_template(true)
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(move |app, event| {
let id = event.id.as_ref();
if id == "show" {
show_main_window(app);
} else if id == "quit" {
is_quitting_menu.store(true, Ordering::SeqCst);
app.exit(0);
} else if let Some(view) = id.strip_prefix("nav:") {
show_main_window(app);
let _ = app.emit("dossier:navigate", view.to_string());
} else if let Some(path) = id.strip_prefix("open:") {
// Direkt Rhino starten — kein Show des Launchers
// damit der Tray-Shortcut wirklich schnell ist.
let path = path.to_string();
if let Err(e) = open_rhino_internal(app, &path) {
eprintln!("Tray open failed: {e}");
}
// Recent-Liste lastOpened aktualisieren
let mut recent = list_recent_internal();
let now = chrono::Utc::now().to_rfc3339();
for p in recent.iter_mut() {
if p.path == path { p.last_opened = Some(now.clone()); }
}
// Sortieren: zuletzt geoeffnete oben
recent.sort_by(|a, b| b.last_opened.cmp(&a.last_opened));
if let Ok(raw) = serde_json::to_string_pretty(&recent) {
let _ = fs::write(recent_path(), raw);
}
// Tray neu bauen, sonst spiegelt das Menue nicht
// die neue Reihenfolge wider.
if let Ok(menu) = build_tray_menu(app) {
if let Some(t) = app.tray_by_id("main") {
let _ = t.set_menu(Some(menu));
}
}
}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
show_main_window(tray.app_handle());
}
})
.build(app)?;
Ok(())
}
})
.build(tauri::generate_context!())
.expect("Fehler beim Starten der Tauri-App")
.run(move |_app, event| {
if let tauri::RunEvent::ExitRequested { .. } = event {
is_quitting.store(true, Ordering::SeqCst);
}
});
} }
+33 -5
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Dossier", "productName": "Dossier",
"version": "0.1.0", "version": "0.6.3",
"identifier": "ch.gabrielevarano.dossier", "identifier": "ch.gabrielevarano.dossier",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
@@ -14,12 +14,28 @@
{ {
"label": "main", "label": "main",
"title": "Dossier", "title": "Dossier",
"width": 920, "width": 1080,
"height": 640, "height": 720,
"minWidth": 720, "minWidth": 880,
"minHeight": 480, "minHeight": 520,
"resizable": true, "resizable": true,
"fullscreen": false "fullscreen": false
},
{
"label": "splash",
"url": "splash.html",
"title": "Dossier lädt",
"width": 440,
"height": 190,
"center": true,
"alwaysOnTop": true,
"decorations": false,
"resizable": false,
"skipTaskbar": true,
"visible": false,
"transparent": true,
"shadow": true,
"focus": false
} }
], ],
"security": { "security": {
@@ -29,14 +45,26 @@
"bundle": { "bundle": {
"active": true, "active": true,
"targets": ["app", "dmg"], "targets": ["app", "dmg"],
"createUpdaterArtifacts": true,
"icon": ["icons/icon.png"], "icon": ["icons/icon.png"],
"copyright": "© 2026 Karim Gabriele Varano", "copyright": "© 2026 Karim Gabriele Varano",
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "Dossier Launcher", "shortDescription": "Dossier Launcher",
"longDescription": "Projekt-Launcher fuer das Dossier-Plugin in Rhino 8.", "longDescription": "Projekt-Launcher fuer das Dossier-Plugin in Rhino 8.",
"macOS": {
"signingIdentity": "-"
},
"resources": { "resources": {
"../../dist": "dist", "../../dist": "dist",
"../../rhino": "rhino" "../../rhino": "rhino"
} }
},
"plugins": {
"updater": {
"endpoints": [
"https://git.kgva.ch/karim/DOSSIER/raw/branch/main/latest.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDY3Q0IzQzA4Mjc5NTczOApSV1E0VjNtQ3dMTjhCamZqbElWdDBlQnNNU3ZEZDg0bEp0aGtyRnN1M2ZKZTdJYzV0TUJEUnhxRQo="
}
} }
} }
+1819 -85
View File
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
// Material-Symbols-Outlined-style Icons als Inline-SVG. Keine Font-Loads,
// kein Codepoint-Mapping — sauber zu themen via currentColor + stroke-width.
const PATHS = {
folder: (
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" />
),
edit: (
<>
<path d="M4 20h4l10.5-10.5a2.121 2.121 0 0 0-3-3L5 17v3Z" />
<path d="m13.5 6.5 3 3" />
</>
),
plus: (
<>
<path d="M12 5v14" />
<path d="M5 12h14" />
</>
),
search: (
<>
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.5-3.5" />
</>
),
close: (
<>
<path d="M18 6 6 18" />
<path d="M6 6l12 12" />
</>
),
settings: (
<>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c.36.13.73.31 1.05.55" />
</>
),
layers: (
<>
<path d="m12 2 9 5-9 5-9-5 9-5Z" />
<path d="m3 12 9 5 9-5" />
<path d="m3 17 9 5 9-5" />
</>
),
trash: (
<>
<path d="M3 6h18" />
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
<path d="m19 6-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
</>
),
refresh: (
<>
<path d="M21 12a9 9 0 1 1-3.5-7.1" />
<path d="M21 4v5h-5" />
</>
),
pin: (
<>
<path d="M9 4h6l-1 5 3 3v2H7v-2l3-3-1-5Z" />
<path d="M12 14v6" />
</>
),
pin_filled: (
<path d="M9 4h6l-1 5 3 3v2h-4v6h-2v-6H7v-2l3-3-1-5Z" fill="currentColor" stroke="none" />
),
snapshot: (
<>
<path d="M3 9a2 2 0 0 1 2-2h2.5l1.7-2h5.6l1.7 2H19a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9Z" />
<circle cx="12" cy="13" r="3.5" />
</>
),
tag: (
<>
<path d="M3 12V4h8l10 10-8 8L3 12Z" />
<circle cx="8" cy="8" r="1.4" fill="currentColor" stroke="none" />
</>
),
chevron_down: <path d="m6 9 6 6 6-6" />,
chevron_up: <path d="m6 15 6-6 6 6" />,
};
export default function Icon({ name, size = 16, strokeWidth = 1.6, style }) {
const path = PATHS[name];
if (!path) return null;
return (
<svg
width={size} height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
style={{ flexShrink: 0, ...style }}
>
{path}
</svg>
);
}
+144
View File
@@ -0,0 +1,144 @@
import { useEffect, useState, useCallback } from "react";
import { checkForAppUpdate, installAppUpdate, skipUpdateVersion, isTauri } from "../utils/updater.js";
export default function UpdateNotifier() {
const [update, setUpdate] = useState(null);
const [state, setState] = useState("idle");
const [downloaded, setDownloaded] = useState(0);
const [total, setTotal] = useState(0);
const [error, setError] = useState(null);
const runCheck = useCallback(async ({ silent }) => {
if (!isTauri()) return;
try {
setState("checking");
setError(null);
const res = await checkForAppUpdate({ respectSkip: silent });
if (!res.available) {
setUpdate(null);
setState("idle");
return;
}
setUpdate(res.update);
setState("available");
} catch (e) {
console.error("Update-Check fehlgeschlagen:", e);
setError(String(e?.message || e));
setState("idle");
}
}, []);
useEffect(() => {
const t = setTimeout(() => runCheck({ silent: true }), 1500);
return () => clearTimeout(t);
}, [runCheck]);
useEffect(() => {
const handler = () => runCheck({ silent: false });
window.addEventListener("dossier:check-update", handler);
return () => window.removeEventListener("dossier:check-update", handler);
}, [runCheck]);
const install = async () => {
if (!update) return;
try {
setState("downloading");
setDownloaded(0);
setTotal(0);
await installAppUpdate(update, (event) => {
if (event.event === "Started") {
setTotal(event.data.contentLength || 0);
} else if (event.event === "Progress") {
setDownloaded((d) => d + (event.data.chunkLength || 0));
} else if (event.event === "Finished") {
setState("installing");
}
});
} catch (e) {
console.error("Update-Installation fehlgeschlagen:", e);
setError(String(e?.message || e));
setState("available");
}
};
const skipVersion = () => {
skipUpdateVersion(update?.version);
setUpdate(null);
setState("idle");
};
const later = () => {
setUpdate(null);
setState("idle");
};
if (!isTauri() || !update || state === "idle" || state === "checking") return null;
const isBusy = state === "downloading" || state === "installing";
const pct = total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : null;
return (
<div className="dialog-bg" style={{ zIndex: 300 }}>
<div className="dialog" style={{ width: 460, maxWidth: "92vw" }}>
<header style={{ background: "var(--bg-panel)" }}>
<div style={{ fontSize: 10, letterSpacing: "0.18em", color: "var(--accent)", marginBottom: 4, fontWeight: 600 }}>
UPDATE VERFÜGBAR
</div>
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
<div style={{ fontSize: 18, color: "var(--text)", fontWeight: 500 }}>
Dossier {update.version}
</div>
{update.currentVersion && (
<div style={{ fontSize: 11, color: "var(--text-muted)" }}>von {update.currentVersion}</div>
)}
</div>
</header>
<div className="body">
{update.body && (
<div style={{ display: "flex", gap: 12 }}>
<div style={{ width: 3, flexShrink: 0, background: "var(--accent)", borderRadius: 2 }} />
<div style={{ fontSize: 12, color: "var(--text-muted)", lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{update.body}</div>
</div>
)}
{error && (
<div style={{ padding: "10px 12px", background: "rgba(200, 112, 80, 0.12)", border: "1px solid var(--danger)", borderRadius: 6, fontSize: 12, color: "var(--danger)" }}>
{error}
</div>
)}
{isBusy && (
<div>
<div style={{ fontSize: 11, color: "var(--text-muted)", marginBottom: 6, letterSpacing: "0.04em" }}>
{state === "downloading"
? (pct !== null ? `Wird heruntergeladen … ${pct}%` : "Wird heruntergeladen …")
: "Wird installiert …"}
</div>
<div style={{ height: 4, background: "var(--bg-elev)", borderRadius: 2, overflow: "hidden" }}>
<div style={{
height: "100%",
width: pct !== null ? `${pct}%` : "100%",
background: "var(--accent)",
transition: "width 0.2s",
animation: pct === null ? "dossier-update-pulse 1.2s ease-in-out infinite" : undefined,
}} />
</div>
<style>{`@keyframes dossier-update-pulse { 0%,100% { opacity: 0.5; } 50% { opacity: 1; } }`}</style>
</div>
)}
</div>
<footer style={{ flexDirection: "column", gap: 8, alignItems: "stretch" }}>
<button className="primary pill" style={{ width: "100%" }} onClick={install} disabled={isBusy}>
{isBusy ? "Bitte warten …" : "Jetzt installieren"}
</button>
<div style={{ display: "flex", gap: 8 }}>
<button style={{ flex: 1 }} onClick={later} disabled={isBusy}>Später</button>
<button style={{ flex: 1 }} onClick={skipVersion} disabled={isBusy}>Diese Version überspringen</button>
</div>
</footer>
</div>
</div>
);
}
+911 -96
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
// Shared helpers fuer den Tauri-Updater. Verwendet vom Auto-Check Modal
// (UpdateNotifier) und dem manuellen Check in den Einstellungen.
export const SKIP_KEY = "dossier_update_skipped_version";
export const LAST_CHECK_KEY = "dossier_update_last_check";
export function isTauri() {
return typeof window !== "undefined" && !!window.__TAURI_INTERNALS__;
}
export async function checkForAppUpdate({ respectSkip = true } = {}) {
if (!isTauri()) return { available: false, isTauri: false };
const { check } = await import("@tauri-apps/plugin-updater");
const result = await check();
localStorage.setItem(LAST_CHECK_KEY, new Date().toISOString());
if (!result?.available) return { available: false, update: null, isTauri: true };
if (respectSkip && localStorage.getItem(SKIP_KEY) === result.version) {
return { available: false, update: result, isTauri: true, skipped: true };
}
return { available: true, update: result, isTauri: true };
}
export async function installAppUpdate(update, onProgress) {
await update.downloadAndInstall(onProgress);
const { relaunch } = await import("@tauri-apps/plugin-process");
await relaunch();
}
export function skipUpdateVersion(version) {
if (version) localStorage.setItem(SKIP_KEY, version);
}
export function getLastUpdateCheck() {
return localStorage.getItem(LAST_CHECK_KEY);
}
export function formatLastCheck(iso) {
if (!iso) return "noch nie";
try {
const d = new Date(iso);
return d.toLocaleString("de-CH", {
day: "2-digit", month: "long", year: "numeric",
hour: "2-digit", minute: "2-digit",
});
} catch {
return "—";
}
}
+4 -4
View File
@@ -1,12 +1,12 @@
{ {
"name": "rhino-panel", "name": "dossier",
"version": "0.0.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "rhino-panel", "name": "dossier",
"version": "0.0.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6" "react-dom": "^19.2.6"
+36
View File
@@ -0,0 +1,36 @@
#! python 3
# -*- coding: utf-8 -*-
"""Hilfsscript: alle Dossier-Panel-Registrierungs-Flags clearen + Module
neu laden. Nuetzlich nach Icon-/Layout-Aenderungen. ABER: Rhinos
Panel-Manager cached die Icon-Bindung pro GUID — fuer NEUE Icons hilft
oft nur ein kompletter Rhino-Neustart. Dieses Script ist fuer alles
andere (Geometrie-/Bridge-Aenderungen).
Ausfuehrung in Rhino:
_RunPythonScript "/Users/karim/STUDIO/DOSSIER/rhino/_reset_panels.py"
"""
import importlib, sys
import scriptcontext as sc
_PANELS = ("elemente", "gestaltung", "oberleiste", "massstab", "ausschnitte",
"layouts", "overrides", "werkzeuge", "dimensionen", "ebenen",
"rhinopanel", "panel_base", "overrides_panel")
cleared = []
for k in list(sc.sticky.keys()):
kl = str(k).lower()
if any(p in kl for p in _PANELS):
cleared.append(k)
sc.sticky[k] = None
print("[reset] sticky cleared:", len(cleared))
reloaded = []
for m in list(sys.modules):
if any(p in m for p in _PANELS):
try:
importlib.reload(sys.modules[m])
reloaded.append(m)
except Exception as ex:
print("[reset] reload {}: {}".format(m, ex))
print("[reset] modules reloaded:", len(reloaded))
print("[reset] FERTIG — Panels jetzt neu via _RunPythonScript oeffnen")
+2 -2
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
ausschnitte.py ausschnitte.py
@@ -705,4 +705,4 @@ class AusschnittBridge(panel_base.BaseBridge):
panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge, panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge,
icon_spec=("A", "#c87050")) icon_spec=("crop", "#c87050"))
+1 -1
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
""" """
clean.py clean.py
Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste
+1 -1
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
""" """
clean_layers.py clean_layers.py
Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.) Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.)
+3 -2
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
dimensionen.py dimensionen.py
@@ -609,4 +609,5 @@ def _bridge_factory():
panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR, panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR,
_bridge_factory, icon_spec=("D", "#9e7050")) _bridge_factory,
icon_spec=("aspect_ratio", "#9e7050"))
+3105 -130
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
gestaltung.py gestaltung.py
@@ -1632,4 +1632,4 @@ def _bridge_factory():
panel_base.register_and_open("gestaltung", "GESTALTUNG", PANEL_GUID_STR, _bridge_factory, panel_base.register_and_open("gestaltung", "GESTALTUNG", PANEL_GUID_STR, _bridge_factory,
icon_spec=("G", "#5fa896")) icon_spec=("palette", "#5fa896"))
+2 -2
View File
@@ -1,11 +1,11 @@
# ! python3 #! python 3
""" """
inspect_section.py inspect_section.py
Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log, Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log,
ohne dass irgendein Panel-Setup gebraucht wird. ohne dass irgendein Panel-Setup gebraucht wird.
Aufruf: Aufruf:
_-RunPythonScript "/Users/karim/STUDIO/rhino-panel/rhino/inspect_section.py" _-RunPythonScript "/Users/karim/STUDIO/DOSSIER/rhino/inspect_section.py"
""" """
import Rhino import Rhino
+253 -25
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
layer_builder.py layer_builder.py
@@ -68,6 +68,150 @@ def _add_layer(doc, name, parent_id=None, color=None, lw=None):
return doc.Layers.Add(layer) return doc.Layers.Add(layer)
def _find_hatch_pattern_index(doc, name):
"""Sucht einen Hatch-Pattern-Index per Name (case-insensitive). -1 wenn nicht da."""
if not name or name == "None":
return -1
target = name.strip().lower()
try:
for i in range(doc.HatchPatterns.Count):
hp = doc.HatchPatterns[i]
if hp is None or hp.IsDeleted: continue
if hp.Name and hp.Name.strip().lower() == target:
return i
except Exception as ex:
print("[EBENEN] hatch lookup:", ex)
return -1
def _find_linetype_index(doc, name):
"""Sucht einen Linetype-Index per Name. -1 = ByLayer."""
if not name or name in ("byLayer", "by_layer", "ByLayer"):
return -1
target = name.strip().lower()
try:
for i in range(doc.Linetypes.Count):
lt = doc.Linetypes[i]
if lt is None or lt.IsDeleted: continue
if lt.Name and lt.Name.strip().lower() == target:
return i
except Exception: pass
return -1
def _apply_section_style(doc, layer, section_cfg, layer_color):
"""Setzt einen Custom-SectionStyle auf den Layer aus dem Dossier-section-dict.
Nutzt Rhino-8's Python-3-API (Rhino.DocObjects.SectionStyle +
Layer.SetCustomSectionStyle / RemoveCustomSectionStyle). In IPy 2.7
sind diese Methoden nicht exponiert dort no-op (mit Print-Warnung).
"""
if not section_cfg or not isinstance(section_cfg, dict):
return
has_setter = hasattr(layer, "SetCustomSectionStyle")
has_remover = hasattr(layer, "RemoveCustomSectionStyle")
if not has_setter:
# IPy-2.7-Pfad: API nicht da, leise raus.
return
try:
SS = Rhino.DocObjects.SectionStyle
except Exception as ex:
print("[EBENEN] SectionStyle-Klasse nicht da:", ex); return
# Wenn alles "leer/Default": Custom-Style abschalten
pat = (section_cfg.get("hatchPattern") or "None").strip()
show = section_cfg.get("boundaryShow", True)
if pat == "None" and not show and has_remover:
try: layer.RemoveCustomSectionStyle()
except Exception: pass
return
style = SS()
# --- Hatch ---
if pat and pat != "None":
hp_idx = _find_hatch_pattern_index(doc, pat)
if hp_idx >= 0:
# Property-Name probieren — Rhino-8 hat HatchIndex
for prop in ("HatchIndex", "HatchPatternIndex"):
if hasattr(style, prop):
try: setattr(style, prop, hp_idx); break
except Exception: pass
# Hatch-Scale
for prop in ("HatchScale", "HatchPatternScale"):
if hasattr(style, prop):
try: setattr(style, prop, float(section_cfg.get("hatchScale") or 1.0)); break
except Exception: pass
# Hatch-Rotation (Rhino erwartet Radians — wir bekommen Grad)
import math
rot_deg = float(section_cfg.get("hatchRotation") or 0)
for prop in ("HatchRotation", "HatchAngle"):
if hasattr(style, prop):
try:
setattr(style, prop, math.radians(rot_deg))
break
except Exception: pass
# Hatch-Color (null = ByObject = nicht setzen)
hatch_color = section_cfg.get("hatchColor")
if hatch_color:
for prop in ("HatchColor", "FillColor"):
if hasattr(style, prop):
try: setattr(style, prop, _color(hatch_color)); break
except Exception: pass
# Background
bg = section_cfg.get("background")
if bg in ("object", "byObject"):
for prop in ("BackgroundColorUsage", "FillBackground"):
if hasattr(style, prop):
# Enum-Werte sind versioniert; wir versuchen via int
try: setattr(style, prop, 1); break
except Exception: pass
# --- Boundary ---
if hasattr(style, "BoundaryVisible"):
try: style.BoundaryVisible = bool(show)
except Exception: pass
elif hasattr(style, "ShowBoundary"):
try: style.ShowBoundary = bool(show)
except Exception: pass
if show:
# Boundary color
bc = section_cfg.get("boundaryColor")
if bc:
for prop in ("BoundaryColor", "OutlineColor", "EdgeColor"):
if hasattr(style, prop):
try: setattr(style, prop, _color(bc)); break
except Exception: pass
# Boundary width scale
ws = float(section_cfg.get("boundaryWidthScale") or 1.0)
for prop in ("BoundaryWidthScale", "EdgeWidthScale", "OutlineWidthScale"):
if hasattr(style, prop):
try: setattr(style, prop, ws); break
except Exception: pass
# Linetype
lt = section_cfg.get("boundaryLinetype")
if lt and lt not in ("byLayer", "ByLayer"):
lt_idx = _find_linetype_index(doc, lt)
for prop in ("BoundaryLinetypeIndex", "EdgeLinetypeIndex"):
if hasattr(style, prop):
try: setattr(style, prop, lt_idx); break
except Exception: pass
# Section open objects
soo = bool(section_cfg.get("sectionOpenObjects", True))
for prop in ("SectionOpenObjects", "ClipOpenObjects"):
if hasattr(style, prop):
try: setattr(style, prop, soo); break
except Exception: pass
# Style auf Layer setzen
try:
layer.SetCustomSectionStyle(style)
except Exception as ex:
print("[EBENEN] SetCustomSectionStyle({}): {}".format(layer.Name, ex))
def build_layers(doc, zeichnungsebenen, ebenen): def build_layers(doc, zeichnungsebenen, ebenen):
""" """
Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert
@@ -113,6 +257,13 @@ def build_layers(doc, zeichnungsebenen, ebenen):
sub.PlotWeight = lw sub.PlotWeight = lw
sub.SetUserString("dossier_code", e["code"]) sub.SetUserString("dossier_code", e["code"])
# Section Style anwenden (Py3-only — IPy 2.7 no-op)
try:
_apply_section_style(doc, doc.Layers[sub_idx],
e.get("section"), e.get("color"))
except Exception as ex:
print("[EBENEN] section-style apply ({}): {}".format(sub_name, ex))
doc.Views.Redraw() doc.Views.Redraw()
print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert".format( print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert".format(
len(zeichnungsebenen), len(ebenen))) len(zeichnungsebenen), len(ebenen)))
@@ -241,48 +392,125 @@ def update_clipping_plane(doc, active_z, enabled):
""" """
Erstellt/aktualisiert/entfernt die DOSSIER-Clipping-Plane an OKFF + Schnitthoehe Erstellt/aktualisiert/entfernt die DOSSIER-Clipping-Plane an OKFF + Schnitthoehe
des aktiven Geschosses. Plane zeigt nach +Z, schneidet alles oberhalb weg. des aktiven Geschosses. Plane zeigt nach +Z, schneidet alles oberhalb weg.
Diagnostik via [CLIP]-Praefix in den Print-Ausgaben bei Problemen die
Rhino-Konsole nach 'CLIP' filtern.
""" """
import Rhino.Geometry as rg import Rhino.Geometry as rg
z_name = (active_z or {}).get("name") if active_z else None
print("[CLIP] update: enabled={} active='{}' isGeschoss={} okff={} sh={}".format(
enabled,
z_name,
bool(active_z and active_z.get("isGeschoss")),
(active_z or {}).get("okff"),
(active_z or {}).get("schnitthoehe"),
))
existing = _find_clipping_plane(doc) existing = _find_clipping_plane(doc)
print("[CLIP] existing: {}".format(existing.Id if existing else "none"))
is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None) is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None)
if (not enabled) or (not is_geschoss):
# IMMER vorhandene Plane loeschen — bei Re-Enable wollen wir frische
# vp_ids (alte koennten leer/falsch sein, dann clippt das Replace zwar
# die Geometrie aber keinen Viewport).
if existing is not None: if existing is not None:
try: try:
doc.Objects.Delete(existing.Id, True) doc.Objects.Delete(existing.Id, True)
except Exception: print("[CLIP] alte Plane geloescht")
pass
return
okff = float(active_z.get("okff", 0.0))
sh = float(active_z.get("schnitthoehe", 1.0))
cut_z = okff + sh
plane = rg.Plane(rg.Point3d(0.0, 0.0, cut_z), rg.Vector3d.ZAxis)
du, dv = 50000.0, 50000.0
if existing is not None:
try:
new_surf = rg.PlaneSurface(plane, rg.Interval(-du/2.0, du/2.0), rg.Interval(-dv/2.0, dv/2.0))
doc.Objects.Replace(existing.Id, new_surf)
except Exception as ex: except Exception as ex:
print("[EBENEN] Clip-Update:", ex) print("[CLIP] Delete fehlgeschlagen:", ex)
else:
if (not enabled) or (not is_geschoss):
print("[CLIP] disabled — fertig (enabled={}, isGeschoss={})".format(enabled, is_geschoss))
doc.Views.Redraw()
return
# dict.get(k, default) liefert default NUR wenn Key fehlt — bei
# Key-vorhanden-aber-None gibt's None zurueck. float(None) crasht.
# Daher explizit None-faangen:
okff_raw = active_z.get("okff")
sh_raw = active_z.get("schnitthoehe")
okff = float(okff_raw) if okff_raw is not None else 0.0
sh = float(sh_raw) if sh_raw is not None else 1.0
cut_z = okff + sh
print("[CLIP] cut_z={} (okff={}, schnitthoehe={})".format(cut_z, okff, sh))
# Normal nach -Z = sichtbar bleibt UNTERHALB der Plane (Grundriss-Schnitt:
# man steht auf der Boden-Seite, alles darueber wird weggeschnitten).
# Mit +Z waere es genau umgekehrt (Decke + Rest oben sichtbar).
plane = rg.Plane(rg.Point3d(0.0, 0.0, cut_z), rg.Vector3d(0.0, 0.0, -1.0))
du, dv = 50000.0, 50000.0
# Viewport-IDs sammeln — wir wollen ALLE Modell-Viewports clippen,
# nicht nur den gerade aktiven. Sammeln aus mehreren Quellen +
# dedupen damit die Plane in Top/Front/Right/Perspective gleichzeitig
# wirkt.
vp_ids = [] vp_ids = []
for view in doc.Views: seen = set()
def _add(vpid):
if vpid is None: return
try: try:
vp_ids.append(view.ActiveViewportID) key = str(vpid)
except Exception: except Exception: return
try: vp_ids.append(view.ActiveViewport.Id) if key in seen or vpid == System.Guid.Empty: return
seen.add(key); vp_ids.append(vpid)
# Methode 1: GetViewList — alle Modell-Views (kein Page-Layout)
try:
views = doc.Views.GetViewList(True, False)
for v in views:
try: _add(v.ActiveViewport.Id)
except Exception: pass except Exception: pass
try: _add(v.MainViewport.Id)
except Exception: pass
except Exception as ex:
print("[CLIP] GetViewList Fehler:", ex)
# Methode 2: Iteration ueber Views (Fallback falls GetViewList anders)
try:
for view in doc.Views:
try: _add(view.ActiveViewport.Id)
except Exception: pass
try: _add(view.MainViewport.Id)
except Exception: pass
try: _add(view.ActiveViewportID)
except Exception: pass
except Exception as ex:
print("[CLIP] doc.Views iteration Fehler:", ex)
# Namen fuer Debug-Output sammeln
vp_names = []
try:
for view in doc.Views:
try: vp_names.append(view.ActiveViewport.Name)
except Exception: pass
except Exception: pass
print("[CLIP] {} Viewport-ID(s) gesammelt: {}".format(
len(vp_ids), ", ".join(vp_names) or "(keine Namen)"))
if not vp_ids:
print("[CLIP] WARNUNG: keine Viewports — Plane wuerde nichts schneiden")
try: try:
new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids) new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
if new_id != System.Guid.Empty: if new_id == System.Guid.Empty:
print("[CLIP] AddClippingPlane lieferte Empty Guid — Fehler")
return
obj = doc.Objects.FindId(new_id) obj = doc.Objects.FindId(new_id)
if obj is not None: if obj is None:
print("[CLIP] FindId nach Erstellung lieferte None — Object weg")
return
attrs = obj.Attributes.Duplicate() attrs = obj.Attributes.Duplicate()
attrs.SetUserString(_CLIP_KEY, "1") attrs.SetUserString(_CLIP_KEY, "1")
attrs.Mode = Rhino.DocObjects.ObjectMode.Locked # Mode = Normal damit die Plane in Mac Rhino voll sichtbar ist.
# Locked-Mode rendert auf Mac oft nur ein blasses Edge. Wer den
# Plane-Boundary nicht selektieren will, kann via Layer locken.
attrs.Mode = Rhino.DocObjects.ObjectMode.Normal
doc.Objects.ModifyAttributes(obj, attrs, True) doc.Objects.ModifyAttributes(obj, attrs, True)
print("[EBENEN] Clipping-Plane bei Z={} erstellt".format(cut_z)) print("[CLIP] Plane erstellt: Z={}, ID={}, du/dv={}/{}".format(
cut_z, new_id, du, dv))
except Exception as ex: except Exception as ex:
print("[EBENEN] Clip-Create:", ex) print("[CLIP] AddClippingPlane Fehler:", ex)
doc.Views.Redraw() doc.Views.Redraw()
+3 -2
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
layouts.py layouts.py
@@ -745,4 +745,5 @@ def _bridge_factory():
panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR, panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR,
_bridge_factory, icon_spec=("L", "#7a5fa8")) _bridge_factory,
icon_spec=("view_quilt", "#7a5fa8"))
+2 -2
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
massstab.py massstab.py
@@ -1091,6 +1091,6 @@ def _bridge_factory():
def register_standalone_panel(): def register_standalone_panel():
panel_base.register_and_open("massstab", "MASSSTAB", PANEL_GUID_STR, _bridge_factory, panel_base.register_and_open("massstab", "MASSSTAB", PANEL_GUID_STR, _bridge_factory,
icon_spec=("M", "#c87050")) icon_spec=("straighten", "#c87050"))
# register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE # register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE
+691 -2
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
oberleiste.py oberleiste.py
@@ -33,6 +33,558 @@ def _run(cmd):
print("[OBERLEISTE] RunScript-Fehler ({}): {}".format(cmd, ex)) print("[OBERLEISTE] RunScript-Fehler ({}): {}".format(cmd, ex))
# --- Window-Layout-Management + App-Settings -------------------------------
# Die Settings werden primaer vom Dossier-Launcher (Tauri-App) verwaltet, der
# nach ~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json
# schreibt. Rhino liest hier nur. Fallback auf den alten RhinoPanel-Pfad fuer
# bestehende Installationen.
import json as _json
import subprocess as _subprocess
_LAUNCHER_DIR = os.path.expanduser(
"~/Library/Application Support/ch.gabrielevarano.Dossier")
_LAUNCHER_PATH = os.path.join(_LAUNCHER_DIR, "dossier_settings.json")
_LEGACY_DIR = os.path.expanduser(
"~/Library/Application Support/RhinoPanel")
_LEGACY_PATH = os.path.join(_LEGACY_DIR, "dossier_settings.json")
def _settings_paths():
"""Suchreihenfolge: Launcher zuerst, dann Legacy-RhinoPanel."""
return (_LAUNCHER_PATH, _LEGACY_PATH)
def _settings_load():
"""Laedt App-Settings aus dem Launcher-JSON (oder Legacy-Path).
Normalisiert Keys: Launcher nutzt `windowLayout`, Legacy `defaultLayout`."""
for p in _settings_paths():
try:
if os.path.isfile(p):
with open(p, "rb") as f:
data = _json.loads(f.read().decode("utf-8"))
if not isinstance(data, dict):
continue
# Normalize legacy keys -> launcher keys
if "windowLayout" not in data and "defaultLayout" in data:
data["windowLayout"] = data.get("defaultLayout") or ""
return data
except Exception as ex:
print("[OBERLEISTE] settings_load ({}):".format(p), ex)
return {}
def _settings_save(data):
"""Schreibt in den Launcher-Pfad (primary). Legacy-Pfad wird nicht mehr
beschrieben der Launcher ist die Autoritaet."""
try:
if not os.path.isdir(_LAUNCHER_DIR):
os.makedirs(_LAUNCHER_DIR)
with open(_LAUNCHER_PATH, "wb") as f:
f.write(_json.dumps(data, ensure_ascii=False, indent=2)
.encode("utf-8"))
return True
except Exception as ex:
print("[OBERLEISTE] settings_save:", ex)
return False
def _hex_to_color(hex_str):
"""`#RRGGBB` -> System.Drawing.Color. Liefert None bei ungueltigem Input."""
if not hex_str or not isinstance(hex_str, str): return None
s = hex_str.strip().lstrip("#")
if len(s) == 3:
s = "".join(c + c for c in s)
if len(s) != 6: return None
try:
r = int(s[0:2], 16)
g = int(s[2:4], 16)
b = int(s[4:6], 16)
import System.Drawing as _sd
return _sd.Color.FromArgb(255, r, g, b)
except Exception:
return None
# Mapping Launcher-Key -> AppearanceSettings-Attribut. Ein Eintrag pro Farbe,
# damit wir leicht erweitern/disablen koennen.
_VIEWPORT_COLOR_ATTRS = (
("background", "ViewportBackgroundColor"),
("gridLine", "GridLineColor"),
("gridMajor", "GridMajorLineColor"),
("gridX", "GridXAxisLineColor"),
("gridY", "GridYAxisLineColor"),
("worldX", "WorldCoordIconXAxisColor"),
("worldY", "WorldCoordIconYAxisColor"),
("worldZ", "WorldCoordIconZAxisColor"),
)
def _apply_viewport_colors(cfg):
"""Setzt App-Appearance-Settings aus der dossier_settings.json (viewportColors).
Tolerant: nicht-existierende Felder werden uebersprungen, Plugin bricht
nicht ab. Triggert ein Redraw aller Viewports am Ende."""
colors = cfg.get("viewportColors") or {}
if not isinstance(colors, dict) or not colors: return False
try:
appset = Rhino.ApplicationSettings.AppearanceSettings
except Exception as ex:
print("[OBERLEISTE] AppearanceSettings nicht verfuegbar:", ex)
return False
applied = []
for key, attr in _VIEWPORT_COLOR_ATTRS:
hexv = colors.get(key)
col = _hex_to_color(hexv) if hexv else None
if col is None: continue
try:
setattr(appset, attr, col)
applied.append(key)
except Exception as ex:
print("[OBERLEISTE] view-color {} -> {}: {}".format(key, attr, ex))
if applied:
# Redraw aller Viewports damit die neuen Farben sofort sichtbar werden.
try:
for v in Rhino.RhinoDoc.ActiveDoc.Views: v.Redraw()
except Exception: pass
print("[OBERLEISTE] Viewport-Colors applied:", applied)
return bool(applied)
def _import_display_modes(paths):
"""Importiert eine Liste von .ini-Pfaden via Rhino.Display API.
Liefert die Anzahl erfolgreich importierter Modes."""
if not paths: return 0
count = 0
try:
DMD = Rhino.Display.DisplayModeDescription
except Exception as ex:
print("[OBERLEISTE] DisplayModeDescription nicht verfuegbar:", ex)
return 0
for p in paths:
try:
if not os.path.isfile(p):
print("[OBERLEISTE] Display-Mode-Pfad fehlt:", p); continue
res = DMD.ImportFromFile(p)
if res:
count += 1
print("[OBERLEISTE] Display-Mode importiert:", p)
else:
print("[OBERLEISTE] Display-Mode-Import lieferte False:", p)
except Exception as ex:
print("[OBERLEISTE] Display-Mode-Import-Fehler ({}): {}".format(p, ex))
if count:
# Cache invalidieren damit das Display-Dropdown die Neuen aufnimmt.
try:
global _display_modes_cache
_display_modes_cache = None
except Exception: pass
return count
_THUMB_SIZE = (480, 320) # 3:2 — kompakt fuer Launcher-Cards
def _save_thumbnail(doc):
"""Rendert den aktiven Viewport in eine PNG-Datei neben der .3dm.
Pfad: <doc.Path>.thumb.png wird vom Launcher aufgegriffen."""
try:
if doc is None: return
doc_path = doc.Path
if not doc_path: return # noch-nicht-gespeichertes Doc
view = doc.Views.ActiveView
if view is None: return
w, h = _THUMB_SIZE
try:
import System.Drawing as _sd
size = _sd.Size(int(w), int(h))
except Exception as ex:
print("[OBERLEISTE] thumb size:", ex); return
try:
bmp = view.CaptureToBitmap(size)
except Exception as ex:
print("[OBERLEISTE] CaptureToBitmap:", ex); return
if bmp is None: return
thumb_path = doc_path + ".thumb.png"
try:
import System.Drawing.Imaging as _imaging
bmp.Save(thumb_path, _imaging.ImageFormat.Png)
print("[OBERLEISTE] Thumb gespeichert:", thumb_path)
except Exception as ex:
print("[OBERLEISTE] Thumb-Save:", ex)
finally:
try: bmp.Dispose()
except Exception: pass
except Exception as ex:
print("[OBERLEISTE] _save_thumbnail Fehler:", ex)
def _launch_dossier_app():
"""Versucht den Dossier-Launcher (Tauri-App) zu oeffnen.
Probiert mehrere Pfade installiertes Bundle, Dev-Build, dann
'open -a Dossier' als letzte Option."""
candidates = [
"/Applications/Dossier.app",
os.path.expanduser("~/Applications/Dossier.app"),
os.path.join(_HERE, "..", "launcher", "src-tauri", "target",
"release", "bundle", "macos", "Dossier.app"),
os.path.join(_HERE, "..", "launcher", "src-tauri", "target",
"debug", "bundle", "macos", "Dossier.app"),
]
for c in candidates:
ap = os.path.abspath(c)
if os.path.isdir(ap):
try:
_subprocess.Popen(["open", ap])
print("[OBERLEISTE] Dossier-Launcher gestartet:", ap)
return True
except Exception as ex:
print("[OBERLEISTE] open Dossier-App ({}):".format(ap), ex)
# Letzter Versuch: per App-Name (sucht in /Applications)
try:
_subprocess.Popen(["open", "-a", "Dossier"])
print("[OBERLEISTE] Dossier via 'open -a Dossier' gestartet")
return True
except Exception as ex:
print("[OBERLEISTE] open -a Dossier:", ex)
return False
def _extract_layout_name_from_xml(content):
"""Liest den Display-Namen aus einer Mac-Rhino-Workspace-XML.
Primaer: name="..." Attribut am Root-<RhinoUI>-Element.
Fallback: <locale_1033>...</locale_1033>."""
if not content: return None
import re
m = re.search(r'<RhinoUI\b[^>]*\bname="([^"]+)"', content)
if m:
name = m.group(1).strip()
if name: return name
m = re.search(r'<locale_1033>([^<]+)</locale_1033>', content)
if m:
name = m.group(1).strip()
if name: return name
return None
def _list_window_layouts():
"""Liefert die Namen aller gespeicherten Window-Layouts in Rhino.
Mac Rhino 8 speichert Layouts als XML (Dateiname = GUID) in
settings/Scheme__Default/workspaces/. Display-Namen liegen im
name="..." Attribut der Root-RhinoUI-Tag.
Probiert zusaetzlich die alte API + den .rwl-Pfad als Fallback."""
out = []
# 1) API-Wege (selten erfolgreich auf Mac)
try:
from Rhino import UI as _RUI
for attr in ("LayoutNames", "GetLayoutNames", "MainWindowLayoutNames"):
try:
names = getattr(_RUI.WindowLayout, attr)()
if names:
for n in names:
if n and n not in out: out.append(str(n))
if out:
print("[OBERLEISTE] Layouts via API.{}: {}".format(attr, out))
return out
except Exception: continue
except Exception: pass
# 2) Mac-XML-Workspaces — der eigentliche Speicherort auf Mac Rhino 8.
workspaces_dir = os.path.expanduser(
"~/Library/Application Support/McNeel/Rhinoceros/8.0/"
"settings/Scheme__Default/workspaces")
try:
if os.path.isdir(workspaces_dir):
for fn in os.listdir(workspaces_dir):
if not fn.lower().endswith(".xml"): continue
fp = os.path.join(workspaces_dir, fn)
try:
with open(fp, "rb") as f:
content = f.read().decode("utf-8", errors="replace")
except Exception: continue
name = _extract_layout_name_from_xml(content)
if name and name not in out: out.append(name)
if out:
print("[OBERLEISTE] {} Layouts via XML gefunden".format(len(out)))
except Exception as ex:
print("[OBERLEISTE] workspaces-scan:", ex)
# 3) Legacy .rwl-Pfade (Windows + ggf. aeltere Rhino-Versionen)
candidate_dirs = [
os.path.expanduser(
"~/Library/Application Support/McNeel/Rhinoceros/8.0/UI/MainWindowLayouts"),
os.path.expanduser(
"~/Library/Application Support/McNeel/Rhinoceros/8.0/MainWindowLayouts"),
os.path.expanduser(
"~/Library/Application Support/McNeel/Rhinoceros/8.0/UI/Layouts"),
os.path.expanduser(
"~/AppData/Roaming/McNeel/Rhinoceros/8.0/UI/MainWindowLayouts"),
]
for d in candidate_dirs:
try:
if os.path.isdir(d):
for fn in os.listdir(d):
if fn.lower().endswith(".rwl"):
name = os.path.splitext(fn)[0]
if name and name not in out: out.append(name)
except Exception: continue
if not out:
print("[OBERLEISTE] Keine Layouts gefunden.")
return out
def _layout_name_to_guid(name):
"""Sucht in den Workspace-XMLs den Eintrag, dessen Display-Name `name`
entspricht und liefert die zugehoerige GUID (= Dateiname ohne .xml)."""
if not name: return None
workspaces_dir = os.path.expanduser(
"~/Library/Application Support/McNeel/Rhinoceros/8.0/"
"settings/Scheme__Default/workspaces")
if not os.path.isdir(workspaces_dir): return None
target = name.strip().lower()
try:
for fn in os.listdir(workspaces_dir):
if not fn.lower().endswith(".xml"): continue
fp = os.path.join(workspaces_dir, fn)
try:
with open(fp, "rb") as f:
content = f.read().decode("utf-8", errors="replace")
except Exception: continue
xn = _extract_layout_name_from_xml(content)
if xn and xn.strip().lower() == target:
return os.path.splitext(fn)[0]
except Exception as ex:
print("[OBERLEISTE] name_to_guid:", ex)
return None
def _apply_window_layout(name):
"""Wendet ein benanntes Window-Layout an. Probiert mehrere Wege weil
Mac Rhino 8 keine offizielle Python-API dafuer exponiert und die
Scripted-Commands je nach Rhino-Version variieren. STOP on success
pruefen via RunScript-Return-Value oder via fehlerfreiem API-Call."""
if not name:
print("[OBERLEISTE] apply_window_layout: leerer Name")
return False
guid = _layout_name_to_guid(name)
print("[OBERLEISTE] apply_window_layout: name='{}' guid='{}'".format(
name, guid))
# 1) Direkt ueber Rhino.UI-API per Reflection. Wir loggen WAS gefunden
# wurde damit man bei Misserfolg sieht ob Klassen/Methoden ueberhaupt
# existieren auf der jeweiligen Rhino-Version.
try:
from Rhino import UI as _RUI
api_candidates = []
for cls_name in ("WindowLayout", "WindowLayouts", "MainWindow", "Panels"):
cls = getattr(_RUI, cls_name, None)
if cls is None: continue
for meth_name in ("Restore", "RestoreLayout", "Apply", "ApplyLayout",
"Load", "LoadLayout", "SetActive", "SetActiveLayout"):
meth = getattr(cls, meth_name, None)
if meth is not None:
api_candidates.append((cls_name, meth_name, meth))
if api_candidates:
print("[OBERLEISTE] API-Kandidaten gefunden:", len(api_candidates),
[(c, m) for c, m, _ in api_candidates])
else:
print("[OBERLEISTE] Keine Rhino.UI-API-Kandidaten (Mac Rhino "
"exposed das nicht statisch). Falle auf Scripted Commands.")
# Args zum Probieren: GUID zuerst (falls vorhanden) dann Name.
# Beide als 1-arg Tuple. Doppelte Klammern haben in der alten Version
# zu mix von String/Tuple gefuehrt — hier sauber als Liste of Tuples.
arg_variants = []
if guid: arg_variants.append((guid,))
arg_variants.append((name,))
for cls_name, meth_name, meth in api_candidates:
for arg in arg_variants:
try:
res = meth(*arg)
print("[OBERLEISTE] apply via Rhino.UI.{}.{}({!r}) -> {}".format(
cls_name, meth_name, arg[0], res))
return True
except Exception:
continue
except Exception as ex:
print("[OBERLEISTE] API-Path Fehler:", ex)
# 2) Scripted Rhino-Commands. STOP on success — RunScript liefert bool.
# Beobachtung aus Mac Rhino 8 Logs: _-WindowLayout "<name>" _Enter wirft
# KEINEN Error wenn das Layout greift; bei unbekanntem Namen kommt
# "Window layout '<name>' not found.". RunScript() liefert True wenn
# die Command-Engine die Zeile syntaktisch akzeptiert hat — das ist
# nicht == "Layout applied", aber ein Hinweis. Wir kombinieren mit der
# Beobachtung dass die naechste Command-Variante interpretiert wuerde
# als Fortsetzung der vorigen (interactive prompt) — daher ESC vorab.
def _try_cmd(cmd):
try:
# Vorab Eingabe-Buffer clearen — sonst landet die naechste
# RunScript-Zeile als Antwort in einem evtl. offenen Prompt.
try: Rhino.RhinoApp.SendKeystrokes("\x1b", False)
except Exception: pass
res = Rhino.RhinoApp.RunScript(cmd, False)
print("[OBERLEISTE] RunScript({!r}) -> {}".format(cmd, res))
return bool(res)
except Exception as ex:
print("[OBERLEISTE] RunScript-Fehler ({}): {}".format(cmd, ex))
return False
quoted = '"{}"'.format(name.replace('"', ''))
# Reihenfolge: die wahrscheinlichste Mac-Variante zuerst.
cmd_candidates = [
'_-WindowLayout {} _Enter'.format(quoted),
'_-SetActiveLayout {} _Enter'.format(quoted),
'_-WindowLayout _Restore {} _Enter'.format(quoted),
]
for cmd in cmd_candidates:
if _try_cmd(cmd):
print("[OBERLEISTE] Command erfolgreich, Stop.")
return True
print("[OBERLEISTE] apply_window_layout: kein Weg hat funktioniert. "
"Wenn das Layout im Rhino-UI bekannt ist aber hier nicht greift, "
"manuell via Window-Menue zu wechseln.")
return False
def open_settings_dialog():
"""Oeffnet ein natives Eto-Forms-Fenster mit den Dossier-Einstellungen.
Vorteil gegenueber HTML-Popover: sprengt die WebView-Bounds der Oberleiste
(Popover wuerde abgeschnitten). Aktuell nur Window-Layout-Defaults; spaeter
erweiterbar um weitere App-Settings oder Aufruf des Tauri-Launchers."""
try:
import Eto.Forms as _ef
import Eto.Drawing as _ed
except Exception as ex:
print("[OBERLEISTE] Eto-Import fehlgeschlagen:", ex); return
cfg = _settings_load()
layouts = _list_window_layouts()
dlg = _ef.Form()
dlg.Title = "Dossier — Einstellungen"
try: dlg.ClientSize = _ed.Size(380, 220)
except Exception: pass
try: dlg.Padding = _ed.Padding(12)
except Exception: pass
try: dlg.Topmost = True
except Exception: pass
# State (closures)
state = {
"defaultLayout": cfg.get("windowLayout") or cfg.get("defaultLayout") or "",
"autoApply": bool(cfg.get("autoApplyLayout", False)),
}
# --- Inhalt ---
lbl_section = _ef.Label()
lbl_section.Text = "FENSTER-LAYOUT"
try:
lbl_section.Font = _ed.Font(_ed.FontFamilies.Sans, 9,
_ed.FontStyle.Bold)
lbl_section.TextColor = _ed.Color.FromArgb(140, 140, 140, 255)
except Exception: pass
# Dropdown
lbl_default = _ef.Label()
lbl_default.Text = "Standard:"
combo = _ef.DropDown()
items = ["— (keines)"] + list(layouts or [])
for it in items: combo.Items.Add(it)
sel = 0
if state["defaultLayout"] in layouts:
sel = 1 + layouts.index(state["defaultLayout"])
combo.SelectedIndex = sel
def _on_combo(s, e):
idx = combo.SelectedIndex
state["defaultLayout"] = "" if idx <= 0 else layouts[idx - 1]
combo.SelectedIndexChanged += _on_combo
# Checkbox
chk = _ef.CheckBox()
chk.Text = "Beim Öffnen automatisch anwenden"
chk.Checked = state["autoApply"]
def _on_chk(s, e):
state["autoApply"] = bool(chk.Checked)
chk.CheckedChanged += _on_chk
# Buttons
btn_apply = _ef.Button()
btn_apply.Text = "Jetzt anwenden"
def _on_apply(s, e):
if state["defaultLayout"]:
_apply_window_layout(state["defaultLayout"])
btn_apply.Click += _on_apply
btn_save = _ef.Button()
btn_save.Text = "Speichern"
def _on_save(s, e):
new_cfg = _settings_load()
new_cfg["windowLayout"] = state["defaultLayout"]
new_cfg["autoApplyLayout"] = state["autoApply"]
# Legacy-Key entfernen damit Launcher und Rhino dieselbe Quelle haben
new_cfg.pop("defaultLayout", None)
_settings_save(new_cfg)
# Oberleiste mit-informieren damit das React-State aktualisiert
try:
b = sc.sticky.get("oberleiste_bridge")
if b is not None: b._send_settings_state()
except Exception: pass
try: dlg.Close()
except Exception: pass
btn_save.Click += _on_save
btn_close = _ef.Button()
btn_close.Text = "Schliessen"
def _on_close(s, e):
try: dlg.Close()
except Exception: pass
btn_close.Click += _on_close
# Hinweis bei keinen Layouts
hint = _ef.Label()
if not layouts:
hint.Text = ("Keine gespeicherten Layouts gefunden.\n"
"In Rhino: Window → Window Layouts → Save…")
try:
hint.TextColor = _ed.Color.FromArgb(140, 140, 140, 255)
hint.Font = _ed.Font(_ed.FontFamilies.Sans, 10,
_ed.FontStyle.Italic)
except Exception: pass
# --- Layout via StackLayout ---
layout = _ef.DynamicLayout()
try:
layout.Padding = _ed.Padding(0)
layout.Spacing = _ed.Size(6, 8)
except Exception: pass
layout.AddRow(lbl_section)
layout.AddRow(lbl_default, combo)
layout.AddRow(chk)
layout.AddRow(btn_apply)
if not layouts:
layout.AddRow(hint)
# Spacer
layout.AddRow(None)
# Save-Row rechtsbuendig
btn_row = _ef.DynamicLayout()
try: btn_row.Spacing = _ed.Size(6, 0)
except Exception: pass
btn_row.BeginHorizontal()
btn_row.Add(None, True)
btn_row.Add(btn_close)
btn_row.Add(btn_save)
btn_row.EndHorizontal()
layout.AddRow(btn_row)
dlg.Content = layout
try: dlg.Show()
except Exception as ex:
print("[OBERLEISTE] Settings-Dialog Show:", ex)
def _get_active_viewport_name(): def _get_active_viewport_name():
try: try:
v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView
@@ -235,6 +787,25 @@ class OberleisteBridge(panel_base.BaseBridge):
# nur die "—"-Option und wirkt wie ein toter Button. # nur die "—"-Option und wirkt wie ein toter Button.
self._dm_sent = False self._dm_sent = False
self._commands_sent = False self._commands_sent = False
# Default-Window-Layout anwenden, wenn aktiviert und noch nicht in
# dieser Session geschehen (sticky-flag verhindert Endlos-Schleifen
# falls die Layout-Restoration unsere Panels neu mountet). Layout-Name
# wird vom Launcher unter `windowLayout` geschrieben; Legacy-Key
# `defaultLayout` wird in _settings_load() bereits normalisiert.
try:
cfg = _settings_load()
if not sc.sticky.get("_dossier_layout_applied"):
layout_name = cfg.get("windowLayout") or cfg.get("defaultLayout")
if cfg.get("autoApplyLayout") and layout_name:
sc.sticky["_dossier_layout_applied"] = True
_apply_window_layout(layout_name)
# Viewport-Colors einmalig pro Session auto-applien (wenn aktiviert)
if (cfg.get("autoApplyViewColors") and
not sc.sticky.get("_dossier_view_colors_applied")):
if _apply_viewport_colors(cfg):
sc.sticky["_dossier_view_colors_applied"] = True
except Exception as ex:
print("[OBERLEISTE] auto-apply (layout/colors):", ex)
self._send_state(force=True) self._send_state(force=True)
def handle(self, data): def handle(self, data):
@@ -398,6 +969,40 @@ class OberleisteBridge(panel_base.BaseBridge):
except Exception: except Exception:
pass pass
# --- Settings + Window-Layout -----------------------------------
elif t == "OPEN_SETTINGS":
# Primaerweg: Dossier-Launcher (Tauri-App) oeffnen, dort lebt das
# echte Settings-UI. Wenn der Launcher nicht installiert ist,
# faellt es auf den Eto-Dialog zurueck.
if not _launch_dossier_app():
open_settings_dialog()
elif t == "GET_SETTINGS":
self._send_settings_state()
elif t == "APPLY_LAYOUT":
name = (p.get("name") or "").strip()
if name: _apply_window_layout(name)
elif t == "SAVE_LAYOUT_PREF":
cfg = _settings_load()
if "windowLayout" in p:
cfg["windowLayout"] = (p.get("windowLayout") or "")
elif "defaultLayout" in p:
cfg["windowLayout"] = (p.get("defaultLayout") or "")
if "autoApplyLayout" in p:
cfg["autoApplyLayout"] = bool(p.get("autoApplyLayout"))
_settings_save(cfg)
self._send_settings_state()
def _send_settings_state(self):
"""Schickt App-Settings + verfuegbare Window-Layouts an die UI."""
cfg = _settings_load()
layout_name = cfg.get("windowLayout") or cfg.get("defaultLayout") or ""
self.send("SETTINGS_STATE", {
"layouts": _list_window_layouts(),
"windowLayout": layout_name,
"defaultLayout": layout_name, # legacy alias
"autoApplyLayout": bool(cfg.get("autoApplyLayout", False)),
})
def _send_state(self, force=False): def _send_state(self, force=False):
doc, vp = massstab._active_vp() doc, vp = massstab._active_vp()
info = massstab._compute_scale(doc, vp) info = massstab._compute_scale(doc, vp)
@@ -460,6 +1065,77 @@ class OberleisteBridge(panel_base.BaseBridge):
self._last_state_sig = sig self._last_state_sig = sig
self.send("STATE", info) self.send("STATE", info)
def _check_pending_launcher_signals(self):
"""Pollt dossier_settings.json auf vom Launcher gesetzte Pending-Flags.
Aktuell: pendingApplyLayout, pendingApplyViewColors,
pendingImportDisplayModes. Loescht das jeweilige Flag nach
Verarbeitung damit es nicht jeden Idle erneut feuert."""
try:
cfg = _settings_load()
mutated = False
pend_layout = cfg.get("pendingApplyLayout")
if isinstance(pend_layout, str) and pend_layout:
print("[OBERLEISTE] pendingApplyLayout:", pend_layout)
_apply_window_layout(pend_layout)
cfg.pop("pendingApplyLayout", None)
mutated = True
if cfg.get("pendingApplyViewColors"):
if _apply_viewport_colors(cfg):
print("[OBERLEISTE] pendingApplyViewColors: angewendet")
cfg["pendingApplyViewColors"] = False
mutated = True
modes = cfg.get("pendingImportDisplayModes") or []
if isinstance(modes, list) and modes:
n = _import_display_modes(modes)
print("[OBERLEISTE] pendingImportDisplayModes: {} importiert".format(n))
cfg["pendingImportDisplayModes"] = []
mutated = True
if cfg.get("pendingExportEbenen"):
# User hat im Launcher "Aus laufendem Rhino importieren"
# geklickt — wir lesen die aktuelle Ebenen-Liste aus dem Doc
# und schreiben sie als layerSchema zurueck.
try:
doc = Rhino.RhinoDoc.ActiveDoc
raw = doc.Strings.GetValue("dossier_ebenen")
if raw:
ebenen = _json.loads(raw)
clean = []
for e in (ebenen or []):
if not isinstance(e, dict): continue
code = e.get("code")
name = e.get("name")
color = e.get("color")
lw = e.get("lw")
if not code or not name: continue
if color is None or lw is None: continue
clean.append({
"code": str(code),
"name": str(name),
"color": str(color),
"lw": float(lw),
})
if clean:
cfg["layerSchema"] = clean
print("[OBERLEISTE] Ebenen-Export: {} Sublayer "
"ins Launcher-Schema geschrieben".format(len(clean)))
else:
print("[OBERLEISTE] Ebenen-Export: doc.Strings hatte "
"keine gueltigen Ebenen")
else:
print("[OBERLEISTE] Ebenen-Export: doc.Strings ['dossier_ebenen'] leer")
except Exception as ex:
print("[OBERLEISTE] Ebenen-Export Fehler:", ex)
cfg["pendingExportEbenen"] = False
mutated = True
if mutated: _settings_save(cfg)
except Exception as ex:
print("[OBERLEISTE] check_pending_launcher_signals:", ex)
def tick_idle(self): def tick_idle(self):
# Command-Prompt aendert sich oft schnell -> separater Pfad: wenn sich # Command-Prompt aendert sich oft schnell -> separater Pfad: wenn sich
# der Prompt seit letztem Tick geaendert hat, sofort pushen (ungedrosselt). # der Prompt seit letztem Tick geaendert hat, sofort pushen (ungedrosselt).
@@ -474,6 +1150,8 @@ class OberleisteBridge(panel_base.BaseBridge):
if self._idle_counter < massstab._IDLE_THROTTLE: if self._idle_counter < massstab._IDLE_THROTTLE:
return return
self._idle_counter = 0 self._idle_counter = 0
# Launcher-Signale pruefen (selten genug — gepollt im normalen Throttle).
self._check_pending_launcher_signals()
self._send_state(force=False) self._send_state(force=False)
@@ -497,8 +1175,19 @@ def _install_listeners(bridge):
try: b._send_state(force=True) try: b._send_state(force=True)
except Exception: pass except Exception: pass
def on_end_save(sender, e):
# EndSaveDocument feuert nach erfolgreichem Save. e.Document gibt
# uns den Doc. Wir generieren das Launcher-Thumbnail neben der .3dm.
try:
doc = getattr(e, "Document", None) or Rhino.RhinoDoc.ActiveDoc
_save_thumbnail(doc)
except Exception as ex:
print("[OBERLEISTE] on_end_save:", ex)
Rhino.RhinoApp.Idle += on_idle Rhino.RhinoApp.Idle += on_idle
Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change
try: Rhino.RhinoDoc.EndSaveDocument += on_end_save
except Exception as ex: print("[OBERLEISTE] EndSaveDocument-Hook:", ex)
sc.sticky[flag] = True sc.sticky[flag] = True
print("[OBERLEISTE] Listener aktiv") print("[OBERLEISTE] Listener aktiv")
@@ -510,4 +1199,4 @@ def _bridge_factory():
panel_base.register_and_open("oberleiste", "OBERLEISTE", PANEL_GUID_STR, _bridge_factory, panel_base.register_and_open("oberleiste", "OBERLEISTE", PANEL_GUID_STR, _bridge_factory,
icon_spec=("O", "#2f5d54")) icon_spec=("menu", "#2f5d54"))
+82 -3
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
overrides.py overrides.py
@@ -70,6 +70,9 @@ _GEST_FILL_KEY = "ebenen_fill_hatch_id" # auf Curve
_ORIG_HP = "dossier_or_hatch_pidx" # auf Hatch — original PatternIndex _ORIG_HP = "dossier_or_hatch_pidx" # auf Hatch — original PatternIndex
_ORIG_HS = "dossier_or_hatch_scale" # auf Hatch — original PatternScale _ORIG_HS = "dossier_or_hatch_scale" # auf Hatch — original PatternScale
_HATCH_OVERRIDDEN = "dossier_or_hatch_done" # "1" wenn Hatch aktuell overridden _HATCH_OVERRIDDEN = "dossier_or_hatch_done" # "1" wenn Hatch aktuell overridden
_ORIG_HC_SRC = "dossier_or_hatch_csrc" # auf Hatch — original ColorSource
_ORIG_HC = "dossier_or_hatch_color" # auf Hatch — original Color
_HATCH_COLOR_OVERRIDDEN = "dossier_or_hatch_color_done"
_FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer _FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer
_FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject _FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject
@@ -342,6 +345,7 @@ def _restore_original(doc, obj):
# Hatch separat zuruecksetzen — kann auch ohne Curve-Override # Hatch separat zuruecksetzen — kann auch ohne Curve-Override
# passiert sein (z.B. wenn Override nur den Pattern aendert) # passiert sein (z.B. wenn Override nur den Pattern aendert)
_restore_hatch(doc, obj) _restore_hatch(doc, obj)
_restore_hatch_color(doc, obj)
if a.GetUserString(_OVERRIDDEN) != "1": if a.GetUserString(_OVERRIDDEN) != "1":
return False return False
try: try:
@@ -481,6 +485,74 @@ def _restore_hatch(doc, curve_obj):
return False return False
def _apply_hatch_color_override(doc, curve_obj, color_hex):
"""Setzt ObjectColor + ColorSource des verlinkten Hatches auf color_hex.
Backup wird einmalig auf dem Hatch in UserStrings gesichert."""
h = _find_linked_hatch(doc, curve_obj)
if h is None: return False
try:
ha = h.Attributes
if ha.GetUserString(_HATCH_COLOR_OVERRIDDEN) != "1":
try:
ha.SetUserString(_ORIG_HC_SRC, str(int(ha.ColorSource)))
ha.SetUserString(_ORIG_HC, _color_to_hex(ha.ObjectColor))
ha.SetUserString(_HATCH_COLOR_OVERRIDDEN, "1")
doc.Objects.ModifyAttributes(h, ha, True)
except Exception as ex:
print("[OVERRIDES] hatch-color backup:", ex)
new_a = h.Attributes.Duplicate()
new_a.ColorSource = _FROM_OBJECT
new_a.ObjectColor = _hex_to_color(color_hex)
try:
new_a.PlotColorSource = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject
new_a.PlotColor = new_a.ObjectColor
except Exception: pass
doc.Objects.ModifyAttributes(h, new_a, True)
return True
except Exception as ex:
print("[OVERRIDES] _apply_hatch_color_override:", ex)
return False
def _restore_hatch_color(doc, curve_obj):
"""Stellt ColorSource + ObjectColor des verlinkten Hatches aus Backup
wieder her."""
h = _find_linked_hatch(doc, curve_obj)
if h is None: return False
a = h.Attributes
if a.GetUserString(_HATCH_COLOR_OVERRIDDEN) != "1": return False
try:
orig_src = a.GetUserString(_ORIG_HC_SRC) or "1" # default ColorFromObject
orig_col = a.GetUserString(_ORIG_HC) or "#f5f5f5"
new_a = h.Attributes.Duplicate()
# ColorSource zuruecksetzen — Enum.ToObject ist in IronPython3
# zuverlaessiger als der direkte int->Enum-Konstruktor.
try:
val = int(orig_src)
new_a.ColorSource = System.Enum.ToObject(
Rhino.DocObjects.ObjectColorSource, val)
except Exception:
new_a.ColorSource = _FROM_OBJECT
try:
new_a.ObjectColor = _hex_to_color(orig_col)
except Exception:
new_a.ObjectColor = Drawing.Color.FromArgb(245, 245, 245)
# PlotColor mit-resetten
try:
new_a.PlotColorSource = (
Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject)
new_a.PlotColor = new_a.ObjectColor
except Exception: pass
for k in (_ORIG_HC_SRC, _ORIG_HC, _HATCH_COLOR_OVERRIDDEN):
try: new_a.SetUserString(k, "")
except Exception: pass
doc.Objects.ModifyAttributes(h, new_a, True)
return True
except Exception as ex:
print("[OVERRIDES] _restore_hatch_color:", ex)
return False
def _apply_to_object(doc, obj, overrides): def _apply_to_object(doc, obj, overrides):
"""Setzt die Override-Werte am Objekt. Sichert vorher Originale.""" """Setzt die Override-Werte am Objekt. Sichert vorher Originale."""
if not overrides: return False if not overrides: return False
@@ -498,6 +570,11 @@ def _apply_to_object(doc, obj, overrides):
new_a.PlotColor = col new_a.PlotColor = col
except Exception: pass except Exception: pass
changed = True changed = True
# Verlinkten Hatch (Gestaltung-Fuellung) auch einfaerben — sonst
# bleibt die Fuellung in der Original-Farbe waehrend die Outline schon
# die Override-Farbe traegt.
try: _apply_hatch_color_override(doc, obj, overrides["color"])
except Exception: pass
if "lineweight" in overrides: if "lineweight" in overrides:
try: try:
new_a.PlotWeightSource = _LW_FROM_OBJ new_a.PlotWeightSource = _LW_FROM_OBJ
@@ -574,8 +651,10 @@ def restore_all(doc):
if _restore_original(doc, obj): if _restore_original(doc, obj):
n += 1 n += 1
else: else:
# Vielleicht nur Hatch-Override # Vielleicht nur Hatch-Override (Pattern und/oder Color)
if _restore_hatch(doc, obj): r1 = _restore_hatch(doc, obj)
r2 = _restore_hatch_color(doc, obj)
if r1 or r2:
n += 1 n += 1
try: doc.Views.Redraw() try: doc.Views.Redraw()
except Exception: pass except Exception: pass
+2 -2
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
overrides_panel.py overrides_panel.py
@@ -222,5 +222,5 @@ def _bridge_factory():
panel_base.register_and_open("overrides", "OVERRIDES", PANEL_GUID_STR, _bridge_factory, panel_base.register_and_open("overrides", "OVERRIDES", PANEL_GUID_STR, _bridge_factory,
icon_spec=("V", "#b5621e"), icon_spec=("tune", "#b5621e"),
min_size=(720, 560)) min_size=(720, 560))
+238 -27
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
panel_base.py panel_base.py
@@ -332,62 +332,273 @@ def _hex_rgb(h):
_ICON_CACHE_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel/icons") _ICON_CACHE_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel/icons")
# Versand-Icons im Projekt-Repo. PRIO 1: PNG (vorgerendert, weiss-auf-
# transparent). PRIO 2: SVG (Mac-Rhino kann's via NSImage manchmal nicht
# rendern, daher als Fallback ueber das Font-Glyph hinaus). Beide Ordner
# werden gechecked.
_PANEL_ICONS_PNG_DIR = os.path.join(
os.path.dirname(_HERE), "icons_export", "panel_icons", "png")
_PANEL_ICONS_SVG_DIR = os.path.join(
os.path.dirname(_HERE), "icons_export", "panel_icons", "svg")
def make_panel_icon(letter, bg_hex): def _try_load_png_white(png_path, size):
"""Erzeugt ein Icon (32x32) mit farbigem Quadrat + Buchstabe. """PNG-Datei direkt als Bitmap laden + auf size skalieren. Geht auf
Schreibt es als PNG-Datei auf Disk und laedt es via Eto.Drawing.Icon(path) allen Rhino-Versionen zuverlaessig (PNG-Support ist universal)."""
das ist der zuverlaessigste Weg auf Mac Rhino.
"""
try: try:
size = 32 # 32x32 fuer Retina (wird auf 16pt skaliert dargestellt) bmp_src = drawing.Bitmap(png_path)
if bmp_src is None: return None
target = drawing.Bitmap(size, size,
drawing.PixelFormat.Format32bppRgba)
g = drawing.Graphics(target)
try:
try: g.AntiAlias = True
except Exception: pass
g.DrawImage(bmp_src, 0, 0, size, size)
finally:
g.Dispose()
return target
except Exception as ex:
print("[panel_base] PNG-load failed:", ex)
return None
def _try_load_svg_white(svg_path, size):
"""Laedt eine SVG-Datei und rendert sie als 32x32-Bitmap mit weisser
Fuell-Farbe. Strategie: SVG-Text einlesen, fill="white" in alle <path>-
Elemente injizieren, in den Icon-Cache als temp .svg schreiben und via
Eto.Drawing.Bitmap(path) laden auf Mac geht das via NSImage das SVGs
seit macOS 10.14 unterstuetzt. Liefert Bitmap oder None."""
try:
with open(svg_path, "rb") as f:
txt = f.read().decode("utf-8")
# Path-Elemente weiss faerben (Material-Symbols-SVGs haben default-
# black fill). Naive String-Manipulation reicht — die SVGs sind
# einfach gestrickt (genau ein <path>).
if 'fill=' not in txt:
txt = txt.replace("<path ", '<path fill="#ffffff" ')
if not os.path.isdir(_ICON_CACHE_DIR):
os.makedirs(_ICON_CACHE_DIR)
safe = re.sub(r"[^A-Za-z0-9]", "_",
os.path.splitext(os.path.basename(svg_path))[0])
tmp_path = os.path.join(_ICON_CACHE_DIR, "_svg_" + safe + ".svg")
with open(tmp_path, "wb") as f:
f.write(txt.encode("utf-8"))
# Eto.Drawing.Bitmap aus File-Pfad: nutzt auf Mac NSImage (kann SVG).
try:
bmp_src = drawing.Bitmap(tmp_path)
except Exception:
bmp_src = None
if bmp_src is None: return None
# Auf size x size skalieren — die meisten Material-Symbols haben
# einen 24-Einheiten-Viewbox, wir wollen 32px Output.
target = drawing.Bitmap(size, size,
drawing.PixelFormat.Format32bppRgba)
g = drawing.Graphics(target)
try:
try: g.AntiAlias = True
except Exception: pass
# Transparenter Hintergrund — der Caller composited spaeter
# ueber den farbigen Panel-Hintergrund.
g.DrawImage(bmp_src, 0, 0, size, size)
finally:
g.Dispose()
return target
except Exception as ex:
print("[panel_base] SVG-load failed:", ex)
return None
# Material Symbols Outlined Codepoints fuer die Panel-Icons.
# Quelle: https://fonts.google.com/icons (Codepoint-Tab pro Icon)
# Wenn die Font "Material Symbols Outlined" installiert ist (Mac:
# ~/Library/Fonts/MaterialSymbolsOutlined-Regular.ttf), werden diese
# Glyphen gerendert. Sonst Fallback auf den ersten Buchstaben.
_MATERIAL_CODEPOINTS = {
"foundation": 0xf200,
"view_in_ar": 0xe9fe,
"palette": 0xe40a,
"settings": 0xe8b8,
"straighten": 0xe41c,
"crop": 0xe3be,
"view_quilt": 0xe8f9,
"tune": 0xe429,
"filter_alt": 0xef4f,
"build": 0xe869,
"construction": 0xea3c,
"aspect_ratio": 0xe85b,
"rule": 0xf1c2,
"layers": 0xe53b,
"menu": 0xe5d2,
"design_services": 0xf10a,
"square_foot": 0xea49,
"dashboard": 0xe871,
"category": 0xe574,
}
_MATERIAL_FONT_NAMES = (
"Material Symbols Outlined",
"Material Symbols Rounded",
"Material Icons", # alter Web-Font
)
def _try_material_font():
"""Probiert die Material-Schrift-Namen durch und liefert den ersten der
sich als FontFamily laden laesst None wenn keiner installiert."""
for fam in _MATERIAL_FONT_NAMES:
try:
ff = drawing.FontFamily(fam)
if ff is not None: return fam
except Exception: continue
return None
def _draw_glyph(g, size, font, glyph, fg):
"""Zeichnet Text mittig auf eine Graphics-Surface."""
try:
ts = g.MeasureString(font, glyph)
tx = (size - ts.Width) / 2
ty = (size - ts.Height) / 2
except Exception:
tx, ty = size * 0.18, size * 0.12
g.DrawText(font, fg, float(tx), float(ty), glyph)
def make_panel_icon(name_or_letter, bg_hex):
"""Erzeugt ein 32x32 Panel-Icon. `name_or_letter` kann ein Material-
Icon-Name (z.B. 'foundation', 'palette') ODER ein einzelner Buchstabe
sein. Bei Material-Namen wird die Material-Schrift verwendet; Fallback
auf den ersten Buchstaben wenn die Schrift nicht installiert ist."""
try:
size = 32
bmp = drawing.Bitmap(size, size, drawing.PixelFormat.Format32bppRgba) bmp = drawing.Bitmap(size, size, drawing.PixelFormat.Format32bppRgba)
g = drawing.Graphics(bmp) g = drawing.Graphics(bmp)
used_material = False
try: try:
try: g.AntiAlias = True try: g.AntiAlias = True
except Exception: pass except Exception: pass
r, gg, bl = _hex_rgb(bg_hex) r, gg, bl = _hex_rgb(bg_hex)
bg = drawing.Color.FromArgb(r, gg, bl, 255) bg = drawing.Color.FromArgb(r, gg, bl, 255)
g.FillRectangle(bg, 0, 0, size, size) g.FillRectangle(bg, 0, 0, size, size)
# 0) Versand-Icons aus dem Repo bevorzugen. Zuerst PNG (geht
# auf allen Rhino-Versionen sicher), sonst SVG-Fallback (NSImage
# auf Mac, klappt nur manchmal).
used_svg = False
icon_bmp = None
chosen_path = ""
try: try:
font = drawing.Font(drawing.FontFamilies.Sans, 18, drawing.FontStyle.Bold) png_path = os.path.join(_PANEL_ICONS_PNG_DIR,
except Exception: name_or_letter + ".png")
font = drawing.Font("Helvetica", 18, drawing.FontStyle.Bold) if os.path.isfile(png_path):
icon_bmp = _try_load_png_white(png_path, size - 8)
if icon_bmp is not None: chosen_path = png_path
else: print("[panel_base] PNG geladen aber Bitmap None:",
png_path)
else:
print("[panel_base] PNG nicht gefunden:", png_path)
if icon_bmp is None:
svg_path = os.path.join(_PANEL_ICONS_SVG_DIR,
name_or_letter + ".svg")
if os.path.isfile(svg_path):
icon_bmp = _try_load_svg_white(svg_path, size - 8)
if icon_bmp is not None: chosen_path = svg_path
if icon_bmp is not None:
pad = 4
try: try:
text_size = g.MeasureString(font, letter) g.DrawImage(icon_bmp, pad, pad,
tx = (size - text_size.Width) / 2 size - 2*pad, size - 2*pad)
ty = (size - text_size.Height) / 2 used_svg = True
used_material = True # → kein Letter-Fallback
print("[panel_base] Icon-Pfad: {}{}".format(
name_or_letter, chosen_path))
except Exception as ex:
print("[panel_base] Icon-Composite Fehler:", ex)
except Exception as ex:
print("[panel_base] Icon-Pfad-Check:", ex)
# 1) Material-Icon-Font (wenn keine SVG vorhanden)
mat_cp = _MATERIAL_CODEPOINTS.get(name_or_letter)
if not used_svg and mat_cp is not None:
font_family_name = _try_material_font()
if font_family_name:
try:
ff = drawing.FontFamily(font_family_name)
# FontStyle.None: in Python3 nicht direkt zugreifbar
# (None ist Keyword) → getattr-Workaround, sonst 0
try: fs = getattr(drawing.FontStyle, "None")
except Exception: fs = 0
font = drawing.Font(ff, 20, fs)
glyph = chr(mat_cp)
_draw_glyph(g, size, font, glyph,
drawing.Colors.White)
used_material = True
except Exception as ex:
print("[panel_base] Material-Render Fehler:", ex)
used_material = False
# 2) Fallback: Buchstabe (erstes Zeichen bzw. eingegebener Buchstabe)
if not used_material:
letter = (name_or_letter[:1].upper()
if name_or_letter else "?")
try:
font = drawing.Font(drawing.FontFamilies.Sans, 18,
drawing.FontStyle.Bold)
except Exception: except Exception:
tx, ty = size * 0.18, size * 0.12 font = drawing.Font("Helvetica", 18,
g.DrawText(font, drawing.Colors.White, float(tx), float(ty), letter) drawing.FontStyle.Bold)
_draw_glyph(g, size, font, letter, drawing.Colors.White)
finally: finally:
g.Dispose() g.Dispose()
# PNG auf Disk schreiben — zuverlaessig fuer Mac Eto.Drawing.Icon # PNG auf Disk schreiben — zuverlaessig fuer Mac Eto.Drawing.Icon
try: try:
if not os.path.isdir(_ICON_CACHE_DIR): if not os.path.isdir(_ICON_CACHE_DIR):
os.makedirs(_ICON_CACHE_DIR) os.makedirs(_ICON_CACHE_DIR)
safe = re.sub(r"[^A-Za-z0-9]", "_", letter) if used_svg: tag = "svg"
path = os.path.join(_ICON_CACHE_DIR, "icon_{}_{}.png".format( elif used_material: tag = "mat"
safe, bg_hex.lstrip("#"))) else: tag = "ltr"
safe = re.sub(r"[^A-Za-z0-9]", "_", name_or_letter)
path = os.path.join(_ICON_CACHE_DIR, "icon_{}_{}_{}.png".format(
tag, safe, bg_hex.lstrip("#")))
bmp.Save(path, drawing.ImageFormat.Png) bmp.Save(path, drawing.ImageFormat.Png)
except Exception as ex: except Exception as ex:
print("[panel_base] Icon-Save:", ex) print("[panel_base] Icon-Save:", ex)
path = None path = None
# 1. Versuch: Icon aus Datei-Pfad # WICHTIG: Mac Rhinos RegisterPanel meldet "expected Icon, got Icon"
# wenn wir Eto.Drawing.Icon uebergeben — die API erwartet
# System.Drawing.Icon. Daher zuerst System.Drawing probieren,
# dann Eto als Fallback.
if path and os.path.isfile(path): if path and os.path.isfile(path):
try: try:
return drawing.Icon(path) import System.Drawing as _sd
ic = _sd.Icon(path)
print("[panel_base] Icon erzeugt via System.Drawing.Icon(path) [{}]".format(tag))
return ic
except Exception as ex: except Exception as ex:
print("[panel_base] Icon(path) fehlgeschlagen:", ex) print("[panel_base] System.Drawing.Icon(path) fehlgeschlagen:", ex)
# 2. Versuch: Icon(scale, bitmap) # System.Drawing.Bitmap als Fallback (manche RegisterPanel-Overloads akzeptieren Bitmap)
try: try:
return drawing.Icon(1.0, bmp) import System.Drawing as _sd
except Exception: pass bmp_sd = _sd.Bitmap(path)
# 3. Versuch: Icon(bitmap) print("[panel_base] Icon erzeugt via System.Drawing.Bitmap(path) [{}]".format(tag))
return bmp_sd
except Exception as ex:
print("[panel_base] System.Drawing.Bitmap(path) fehlgeschlagen:", ex)
# Eto.Drawing.Icon als letzter Versuch — falls Rhino-Version anders ist
try: try:
return drawing.Icon(bmp) ic = drawing.Icon(path)
print("[panel_base] Icon erzeugt via Eto.Drawing.Icon(path) [{}]".format(tag))
return ic
except Exception as ex:
print("[panel_base] Eto.Drawing.Icon(path) fehlgeschlagen:", ex)
# Bitmap-Fallback (in-memory) — wenn alles vorherige fehlschlaegt
try:
ic = drawing.Icon(1.0, bmp)
print("[panel_base] Icon erzeugt via Eto.Drawing.Icon(scale, bmp) [{}]".format(tag))
return ic
except Exception: pass except Exception: pass
# 4. Fallback: einfach das Bitmap zurueck (Rhino akzeptiert ggf. das auch) print("[panel_base] Icon Fallback: Eto.Bitmap zurueck ({})".format(tag))
return bmp return bmp
except Exception as ex: except Exception as ex:
print("[panel_base] Icon-Erstellung fehlgeschlagen:", ex) print("[panel_base] Icon-Erstellung fehlgeschlagen:", ex)
+91 -3
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
rhinopanel.py rhinopanel.py
@@ -44,6 +44,34 @@ def _hatch_pattern_names(doc):
return out return out
def _read_launcher_schema():
"""Liest das Default-Layer-Schema aus dossier_settings.json (Launcher-Pfad).
Liefert eine Liste {code, name, color, lw} oder None wenn nicht gesetzt."""
paths = [
os.path.expanduser("~/Library/Application Support/"
"ch.gabrielevarano.Dossier/dossier_settings.json"),
os.path.expanduser("~/Library/Application Support/"
"RhinoPanel/dossier_settings.json"),
]
for p in paths:
try:
if not os.path.isfile(p): continue
with open(p, "rb") as f:
data = json.loads(f.read().decode("utf-8"))
schema = (data or {}).get("layerSchema")
if isinstance(schema, list) and schema:
# Sanity: alle vier Pflichtfelder vorhanden
clean = [r for r in schema
if isinstance(r, dict)
and r.get("code") and r.get("name")
and r.get("color") is not None
and r.get("lw") is not None]
if clean: return clean
except Exception as ex:
print("[EBENEN] launcher-schema lesen ({}):".format(p), ex)
return None
class EbenenBridge(panel_base.BaseBridge): class EbenenBridge(panel_base.BaseBridge):
def __init__(self): def __init__(self):
panel_base.BaseBridge.__init__(self, "ebenen") panel_base.BaseBridge.__init__(self, "ebenen")
@@ -68,7 +96,15 @@ class EbenenBridge(panel_base.BaseBridge):
except Exception as ex: except Exception as ex:
print("[EBENEN] State-Sync:", ex) print("[EBENEN] State-Sync:", ex)
else: else:
self.send("FIRST_RUN", {"hatchPatterns": _hatch_pattern_names(doc)}) payload = {"hatchPatterns": _hatch_pattern_names(doc)}
# Falls der User im Launcher eigene Default-Ebenen definiert hat,
# mitschicken — React nimmt's statt seiner hardcoded INITIAL_EBENEN.
launcher_schema = _read_launcher_schema()
if launcher_schema:
payload["defaultEbenen"] = launcher_schema
print("[EBENEN] FIRST_RUN mit Launcher-Schema ({} Ebenen)".format(
len(launcher_schema)))
self.send("FIRST_RUN", payload)
def handle(self, data): def handle(self, data):
if not isinstance(data, dict): if not isinstance(data, dict):
@@ -103,6 +139,11 @@ class EbenenBridge(panel_base.BaseBridge):
self._move_selection_to_layer(p.get("code", "")) self._move_selection_to_layer(p.get("code", ""))
elif t == "SET_VISIBILITY": elif t == "SET_VISIBILITY":
self._apply_visibility(p) self._apply_visibility(p)
elif t == "SET_CLIPPING":
# Toggle ohne Full-Apply — wirkt live auf das aktuell aktive
# Geschoss. Erwartet payload {enabled: bool}.
enabled = bool(p.get("enabled"))
self._toggle_clipping_for_active(enabled)
# --- Ebenen-Kombinationen (geteilter Store mit Ausschnitten) ------- # --- Ebenen-Kombinationen (geteilter Store mit Ausschnitten) -------
elif t == "GET_COMBINATION": elif t == "GET_COMBINATION":
self._send_combination() self._send_combination()
@@ -315,6 +356,45 @@ class EbenenBridge(panel_base.BaseBridge):
print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex)) print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex))
print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated)) print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated))
def _toggle_clipping_for_active(self, enabled):
"""Setzt hasClipping fuer das aktuell aktive Geschoss + persistiert in
doc.Strings + triggert plane-update. Wird vom React-Toggle 'Clipping
Plane' direkt aufgerufen (ohne Full-Apply)."""
doc = Rhino.RhinoDoc.ActiveDoc
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
active_id = doc.Strings.GetValue("dossier_active_id")
if not z_raw or not active_id:
print("[CLIP] toggle: kein aktives Geschoss")
return
try:
z_list = json.loads(z_raw)
except Exception as ex:
print("[CLIP] toggle JSON-decode:", ex); return
active_z = None
for z in z_list:
if z.get("id") == active_id:
z["hasClipping"] = bool(enabled)
active_z = z
break
if active_z is None:
print("[CLIP] toggle: active_id={} nicht in Liste".format(active_id))
return
try:
doc.Strings.SetString("dossier_zeichnungsebenen",
json.dumps(z_list, ensure_ascii=False))
except Exception as ex:
print("[CLIP] toggle SetString:", ex)
self._update_clipping(active_z=active_z)
# State an React zurueckspiegeln, damit das Eye-Icon im GeschossManager
# synchron bleibt.
try:
self.send("STATE_SYNC", {
"zeichnungsebenen": z_list,
"ebenen": json.loads(doc.Strings.GetValue("dossier_ebenen") or "[]"),
"hatchPatterns": _hatch_pattern_names(doc),
})
except Exception: pass
def _update_clipping(self, active_z=None): def _update_clipping(self, active_z=None):
"""Clipping-Plane folgt aktivem Geschoss — nur wenn dessen hasClipping=True.""" """Clipping-Plane folgt aktivem Geschoss — nur wenn dessen hasClipping=True."""
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
@@ -327,6 +407,14 @@ class EbenenBridge(panel_base.BaseBridge):
active_z = next((z for z in z_list if z.get("id") == active_id), None) active_z = next((z for z in z_list if z.get("id") == active_id), None)
except Exception: except Exception:
active_z = None active_z = None
# Volles Dump des Active-Geschosses fuer Diagnose. Wenn hier
# hasClipping fehlt aber im UI gesetzt wurde, hat React's Dialog die
# Daten nicht weitergereicht (haeufig: WebView-Cache zeigt alte JS).
try:
print("[CLIP] active_z keys: {}".format(
sorted(active_z.keys()) if active_z else None))
print("[CLIP] active_z dump: {}".format(json.dumps(active_z, ensure_ascii=False)))
except Exception: pass
enabled = bool(active_z and active_z.get("hasClipping")) enabled = bool(active_z and active_z.get("hasClipping"))
_set_processing(True) _set_processing(True)
try: try:
@@ -795,4 +883,4 @@ def _install_layer_listener(bridge):
panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory, panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory,
icon_spec=("E", "#3a6fa8")) icon_spec=("layers", "#3a6fa8"))
+23 -1
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
startup.py startup.py
@@ -13,6 +13,12 @@ import json
import Rhino import Rhino
import scriptcontext as sc import scriptcontext as sc
# DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start.
print("[STARTUP] Python: {}".format(sys.version))
print("[STARTUP] Implementation: {}".format(
sys.implementation.name if hasattr(sys, "implementation") else "n/a (IPy2)"))
print("[STARTUP] Platform: {}".format(sys.platform))
_HERE = os.path.dirname(os.path.abspath(__file__)) _HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path: if _HERE not in sys.path:
sys.path.insert(0, _HERE) sys.path.insert(0, _HERE)
@@ -129,6 +135,22 @@ def _load_all(sender, e):
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex)) print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden # DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
_hint_dossier_ui() _hint_dossier_ui()
# Marker fuer den Launcher-Splash mit Verzoegerung: erst nachdem Rhino die
# Panels visuell platziert hat (~2s nach Modul-Imports). Pfad ist projekt-
# stabil (gleich wie dossier_settings.json), damit Launcher ohne
# Konfiguration weiss wohin er pollt.
def _write_marker():
try:
marker_dir = os.path.expanduser(
"~/Library/Application Support/ch.gabrielevarano.Dossier"
)
if os.path.isdir(marker_dir):
with open(os.path.join(marker_dir, "plugin_loaded.flag"), "w") as f:
f.write("ok")
except Exception as ex:
print("[STARTUP] marker schreiben:", ex)
import threading
threading.Timer(2.0, _write_marker).start()
print("[STARTUP] Fertig") print("[STARTUP] Fertig")
+142
View File
@@ -0,0 +1,142 @@
# ! python3
# -*- coding: utf-8 -*-
"""
startup.py
Laedt DOSSIER-Panels beim Rhino-Start. Liest pro geoeffnetem Dokument eine
`dossier.project.json` (neben der `.3dm` abgelegt vom Dossier-Launcher) und
aktiviert nur die dort gelisteten Module. Fehlt die Datei → alle bekannten
Module laden (Backwards-Compat fuer Setups ohne Launcher).
"""
import os
import sys
import json
import Rhino
import scriptcontext as sc
# DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start.
print("[STARTUP] Python: {}".format(sys.version))
print("[STARTUP] Implementation: {}".format(
sys.implementation.name if hasattr(sys, "implementation") else "n/a (IPy2)"))
print("[STARTUP] Platform: {}".format(sys.platform))
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
# Pfad zur Custom-UI (Toolbars/Sidebar) — wird einmal pro Session geladen
_UI_FILE = os.path.join(_HERE, "DOSSIERUI.rhw")
# Map: Modul-ID (aus dossier.project.json) -> Python-Modulname (Datei in rhino/).
# Muss synchron sein mit launcher/modules.json. Wenn neue Module dazukommen,
# beide Stellen pflegen.
_MODULE_TO_PY = {
"ebenen": "rhinopanel",
"oberleiste": "oberleiste",
"ausschnitte": "ausschnitte",
"gestaltung": "gestaltung",
"werkzeuge": "werkzeuge",
"overrides": "overrides_panel",
"dimensionen": "dimensionen",
"layouts": "layouts",
"elemente": "elemente",
}
_ALL_MODULES = list(_MODULE_TO_PY.keys())
def _read_project_config():
"""Liest dossier.project.json aus dem Ordner des aktiven Docs. Rueckgabe:
dict oder None. None heisst „keine Config" -> Fallback alle Module."""
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None or not getattr(doc, "Path", None):
return None
doc_dir = os.path.dirname(doc.Path)
if not doc_dir:
return None
config_path = os.path.join(doc_dir, "dossier.project.json")
if not os.path.isfile(config_path):
return None
with open(config_path, "rb") as f:
data = json.loads(f.read().decode("utf-8"))
return data if isinstance(data, dict) else None
except Exception as ex:
print("[STARTUP] Project-Config lesen:", ex)
return None
def _migrate_active_doc(*_):
"""Migriert Legacy-Keys (traite_*, pause_*) -> dossier_* fuer das aktive Doc."""
try:
import panel_base
panel_base.migrate_to_dossier(Rhino.RhinoDoc.ActiveDoc)
except Exception as ex:
print("[STARTUP] Migration:", ex)
def _on_doc_opened(sender, e):
"""Greift bei jedem geoeffneten Doc nach Rhino-Start. Migration ist
idempotent (Flag in doc.Strings)."""
try:
doc = e.Document if hasattr(e, "Document") else Rhino.RhinoDoc.ActiveDoc
import panel_base
panel_base.migrate_to_dossier(doc)
except Exception as ex:
print("[STARTUP] _on_doc_opened:", ex)
def _hint_dossier_ui():
"""Mac Rhino 8 kann Window-Layout-Dateien nicht via Skript laden — der
Dialog ueber Window-Menue nutzt interne API ohne Command-Echo. Wir
geben nur einen Hinweis-Pfad aus, damit der User DOSSIERUI.rhw einmal
manuell laden kann. Rhino merkt sich die Anordnung dann persistent."""
if not os.path.isfile(_UI_FILE):
return
print("[STARTUP] DOSSIERUI gefunden: {}".format(_UI_FILE))
print("[STARTUP] Einmalig laden: Window -> Window Layout -> Open -> obige Datei")
print("[STARTUP] Anordnung bleibt danach ueber Rhino-Restarts erhalten.")
def _load_all(sender, e):
"""Wird beim ersten Idle ausgefuehrt — entkoppelt sich danach selbst."""
try:
Rhino.RhinoApp.Idle -= _load_all
except Exception:
pass
print("[STARTUP] Lade DOSSIER-Panels...")
# Migration einmal fuer das beim Start aktive Doc
_migrate_active_doc()
# Und Listener fuer spaeter geoeffnete Docs registrieren
try:
Rhino.RhinoDoc.EndOpenDocument += _on_doc_opened
except Exception as ex:
print("[STARTUP] EndOpenDocument-Hook:", ex)
# Projekt-Config bestimmt, welche Module geladen werden. Ohne Config
# (kein Launcher benutzt, oder Datei nicht da) laedt der Host alles.
config = _read_project_config()
if config and isinstance(config.get("modules"), list):
enabled_ids = [m for m in config["modules"] if m in _MODULE_TO_PY]
unknown = [m for m in config["modules"] if m not in _MODULE_TO_PY]
print("[STARTUP] Projekt: '{}'".format(config.get("name") or "?"))
print("[STARTUP] Aktivierte Module: {}".format(", ".join(enabled_ids) or "(keine)"))
if unknown:
print("[STARTUP] Unbekannte Modul-IDs in Config: {}".format(unknown))
else:
enabled_ids = _ALL_MODULES
print("[STARTUP] Keine dossier.project.json — alle Module laden")
# massstab.py wird als Library mitgeladen (von oberleiste/ausschnitte/...)
# und braucht hier nicht mehr als eigenstaendiges Panel zu erscheinen.
for mod_id in enabled_ids:
py_name = _MODULE_TO_PY[mod_id]
try:
__import__(py_name)
print("[STARTUP] {} ({}) OK".format(mod_id, py_name))
except Exception as ex:
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
_hint_dossier_ui()
print("[STARTUP] Fertig")
Rhino.RhinoApp.Idle += _load_all
print("[STARTUP] geplant - laedt sobald Rhino idle ist")
+2 -2
View File
@@ -1,4 +1,4 @@
# ! python3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
werkzeuge.py werkzeuge.py
@@ -55,4 +55,4 @@ def _bridge_factory():
panel_base.register_and_open("werkzeuge", "WERKZEUGE", PANEL_GUID_STR, _bridge_factory, panel_base.register_and_open("werkzeuge", "WERKZEUGE", PANEL_GUID_STR, _bridge_factory,
icon_spec=("W", "#3a6fa8")) icon_spec=("build", "#3a6fa8"))
+26 -6
View File
@@ -92,10 +92,21 @@ export default function App() {
setCombDialog(d => d ? { ...d, layers: layers || d.layers, presets: presets || [] } : d) setCombDialog(d => d ? { ...d, layers: layers || d.layers, presets: presets || [] } : d)
} }
}) })
onMessage('FIRST_RUN', () => { onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => {
applyAll(INITIAL_ZEICHNUNGSEBENEN, INITIAL_EBENEN) // Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir
// das statt der hardcoded INITIAL_EBENEN. Felder ohne `visible`/`locked`
// werden mit Defaults ergaenzt damit die UI-Komponenten keine undefineds
// sehen.
const useEbenen = (Array.isArray(defaultEbenen) && defaultEbenen.length)
? defaultEbenen.map(e => ({
visible: true, locked: false,
...e,
}))
: INITIAL_EBENEN
setEbenen(useEbenen)
applyAll(INITIAL_ZEICHNUNGSEBENEN, useEbenen)
setAppliedZ(INITIAL_ZEICHNUNGSEBENEN) setAppliedZ(INITIAL_ZEICHNUNGSEBENEN)
setAppliedE(INITIAL_EBENEN) setAppliedE(useEbenen)
const active = INITIAL_ZEICHNUNGSEBENEN.find(zz => zz.id === activeId) || INITIAL_ZEICHNUNGSEBENEN[0] const active = INITIAL_ZEICHNUNGSEBENEN.find(zz => zz.id === activeId) || INITIAL_ZEICHNUNGSEBENEN[0]
if (active) { if (active) {
setActiveZeichnungsebene(active) setActiveZeichnungsebene(active)
@@ -136,13 +147,22 @@ export default function App() {
if (!f || !f.pattern || f.pattern === 'None') return '' if (!f || !f.pattern || f.pattern === 'None') return ''
return [f.pattern, f.source || 'layer', f.color || '', f.scale ?? 1, f.rotation ?? 0].join('|') return [f.pattern, f.source || 'layer', f.color || '', f.scale ?? 1, f.rotation ?? 0].join('|')
} }
// WICHTIG: alle Felder die das Backend braucht hier mit drin haben sonst
// triggert Aenderung an z.B. hasClipping/schnitthoehe kein Apply, und das
// Backend sieht den neuen Stand nie. Frueher waren nur id/name/isGeschoss
// drin -> Clipping-Toggle blieb wirkungslos.
const zSig = (z) => [
z.id, z.name, z.isGeschoss ? 1 : 0,
z.hoehe ?? '', z.schnitthoehe ?? '',
z.hasClipping ? 1 : 0,
].join(':')
const structureKey = useMemo(() => ( const structureKey = useMemo(() => (
zeichnungsebenen.map(z => `${z.id}:${z.name}:${z.isGeschoss ? 1 : 0}`).join(',') + '|' + zeichnungsebenen.map(zSig).join(',') + '|' +
ebenen.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',') ebenen.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',')
), [zeichnungsebenen, ebenen]) ), [zeichnungsebenen, ebenen])
const appliedStructureKey = useMemo(() => ( const appliedStructureKey = useMemo(() => (
appliedZ.map(z => `${z.id}:${z.name}:${z.isGeschoss ? 1 : 0}`).join(',') + '|' + appliedZ.map(zSig).join(',') + '|' +
appliedE.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',') appliedE.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',')
), [appliedZ, appliedE]) ), [appliedZ, appliedE])
@@ -176,7 +196,7 @@ export default function App() {
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim() const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
if (!name) return if (!name) return
if (combinations.some(p => p.name === name) && if (combinations.some(p => p.name === name) &&
!window.confirm(`"${name}" ueberschreiben?`)) return !window.confirm(`"${name}" überschreiben?`)) return
saveCurrentAsCombination(name) saveCurrentAsCombination(name)
setActiveCombName(name) setActiveCombName(name)
} }
+1 -1
View File
@@ -228,7 +228,7 @@ export default function DimensionenApp() {
<Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} /> <Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
<div style={{ marginTop: 8 }}>Keine Selektion.</div> <div style={{ marginTop: 8 }}>Keine Selektion.</div>
<div style={{ marginTop: 4, fontSize: 10 }}> <div style={{ marginTop: 4, fontSize: 10 }}>
In Rhino ein oder mehrere Objekte auswaehlen. In Rhino ein oder mehrere Objekte auswählen.
</div> </div>
</div> </div>
) : ( ) : (
+651 -48
View File
@@ -3,7 +3,9 @@ import Icon from './components/Icon'
import { import {
onMessage, notifyReady, onMessage, notifyReady,
listElemente, createWall, createDecke, createDach, listElemente, createWall, createDecke, createDach,
createFenster, createTuer, createTreppe, createFenster, createTuer, createAussparung, createTreppe,
createStuetze, createTraeger, createRaum,
exportRaeume,
updateElement, deleteElement, regenerateAllElements, updateElement, deleteElement, regenerateAllElements,
} from './lib/rhinoBridge' } from './lib/rhinoBridge'
@@ -167,8 +169,34 @@ const KIND_META = {
decke: { icon: 'layers', label: 'Decke', color: '#b8a890' }, decke: { icon: 'layers', label: 'Decke', color: '#b8a890' },
dach: { icon: 'roofing', label: 'Dach', color: '#c89878' }, dach: { icon: 'roofing', label: 'Dach', color: '#c89878' },
fenster: { icon: 'window', label: 'Fenster', color: '#90b8d0' }, fenster: { icon: 'window', label: 'Fenster', color: '#90b8d0' },
tuer: { icon: 'sensor_door', label: 'Tuer', color: '#c8a878' }, tuer: { icon: 'sensor_door', label: 'Tür', color: '#c8a878' },
treppe: { icon: 'stairs', label: 'Treppe', color: '#a0c0a0' }, treppe: { icon: 'stairs', label: 'Treppe', color: '#a0c0a0' },
stuetze: { icon: 'square_foot', label: 'Stütze', color: '#5fa896' },
traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#7fc8a8' },
raum: { icon: 'crop_free', label: 'Raum', color: '#a0a8b0' },
aussparung: { icon: 'rectangle', label: 'Aussparung', color: '#9090a0' },
}
const RAUM_RUNDUNGEN = ['exakt', '0.01', '0.1', '0.5', '1']
const RAUM_ALIGN = [
{ code: 'links', label: 'Links', icon: 'format_align_left' },
{ code: 'mid', label: 'Mitte', icon: 'format_align_center' },
{ code: 'rechts', label: 'Rechts', icon: 'format_align_right' },
]
const RAUM_SIA_KINDS = [
{ code: '', label: '—', color: 'transparent', hint: '' },
{ code: 'hnf', label: 'HNF', color: '#e8a8a8', hint: 'Hauptnutzfläche' },
{ code: 'nnf', label: 'NNF', color: '#e8c498', hint: 'Nebennutzfläche' },
{ code: 'vf', label: 'VF', color: '#e8d878', hint: 'Verkehrsfläche' },
{ code: 'ff', label: 'FF', color: '#a8c8e0', hint: 'Funktionsfläche' },
]
const PROFIL_META = {
quadrat: { label: 'Quadrat', icon: 'square' },
rechteck: { label: 'Rechteck', icon: 'rectangle' },
rund: { label: 'Rund', icon: 'circle' },
i_profil: { label: 'I-Profil', icon: 'view_column' },
rohr: { label: 'Rohr', icon: 'radio_button_unchecked' },
} }
function ElementList({ elements }) { function ElementList({ elements }) {
@@ -179,7 +207,8 @@ function ElementList({ elements }) {
if (!grouped[k]) grouped[k] = [] if (!grouped[k]) grouped[k] = []
grouped[k].push(el) grouped[k].push(el)
} }
const kindOrder = ['wand', 'decke', 'dach', 'fenster', 'tuer', 'treppe'] const kindOrder = ['wand', 'decke', 'dach', 'fenster', 'tuer', 'aussparung',
'treppe', 'stuetze', 'traeger', 'raum']
return ( return (
<div style={{ <div style={{
display: 'flex', flexDirection: 'column', gap: 8, display: 'flex', flexDirection: 'column', gap: 8,
@@ -229,6 +258,19 @@ function ElementListRow({ el, meta }) {
return `${el.nStufen} St · H ${fmtNum(el.ok - el.uk)} m` return `${el.nStufen} St · H ${fmtNum(el.ok - el.uk)} m`
if (el.kind === 'dach' && el.neigung != null) if (el.kind === 'dach' && el.neigung != null)
return `d ${fmtNum(el.dicke)} · ${fmtNum(el.neigung)}°` return `d ${fmtNum(el.dicke)} · ${fmtNum(el.neigung)}°`
if (el.kind === 'stuetze' || el.kind === 'traeger') {
const pl = (PROFIL_META[el.profil] || {}).label || el.profil
const dim = (el.profil === 'rund' || el.profil === 'rohr')
? `Ø${fmtNum(el.d)}`
: `${fmtNum(el.b)}×${fmtNum(el.h)}`
return `${pl} ${dim}`
}
if (el.kind === 'raum') {
const label = el.nummer ? `${el.nummer} ${el.name}` : el.name
return label || 'Raum'
}
if (el.kind === 'aussparung')
return `${fmtNum(el.area)}`
return `d ${fmtNum(el.dicke)} m` return `d ${fmtNum(el.dicke)} m`
})() })()
const tertiary = (() => { const tertiary = (() => {
@@ -236,6 +278,17 @@ function ElementListRow({ el, meta }) {
return `Br ${fmtNum(el.brueest)}` return `Br ${fmtNum(el.brueest)}`
if (el.kind === 'tuer' || el.kind === 'treppe') if (el.kind === 'tuer' || el.kind === 'treppe')
return '' return ''
if (el.kind === 'stuetze')
return `H ${fmtNum(el.ok - el.uk)} m`
if (el.kind === 'traeger')
return `L ${fmtNum(el.axisLen)} m`
if (el.kind === 'raum') {
const siaInfo = RAUM_SIA_KINDS.find(s => s.code === (el.sia || ''))
const tag = siaInfo && siaInfo.code ? `${siaInfo.label} · ` : ''
return `${tag}${el.areaFmt}`
}
if (el.kind === 'aussparung')
return `U ${fmtNum(el.umfang)} m`
return `UK ${fmtNum(el.uk)} · OK ${fmtNum(el.ok)}` return `UK ${fmtNum(el.uk)} · OK ${fmtNum(el.ok)}`
})() })()
return ( return (
@@ -276,6 +329,8 @@ function ElementListRow({ el, meta }) {
function NeuesElementSection({ noGeschoss, activeName }) { function NeuesElementSection({ noGeschoss, activeName }) {
const [treppeMenuOpen, setTreppeMenuOpen] = useState(false) const [treppeMenuOpen, setTreppeMenuOpen] = useState(false)
const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false)
const [traegerMenuOpen, setTraegerMenuOpen] = useState(false)
const treppeWrapperRef = useRef(null) const treppeWrapperRef = useRef(null)
const dis = noGeschoss const dis = noGeschoss
const baseHint = (label) => const baseHint = (label) =>
@@ -286,6 +341,8 @@ function NeuesElementSection({ noGeschoss, activeName }) {
e.preventDefault() e.preventDefault()
setTreppeMenuOpen(true) setTreppeMenuOpen(true)
} }
const openStuetzeMenu = (e) => { e.preventDefault(); setStuetzeMenuOpen(true) }
const openTraegerMenu = (e) => { e.preventDefault(); setTraegerMenuOpen(true) }
const treppeItems = [ const treppeItems = [
{ icon: 'stairs', label: 'Gerade Treppe', { icon: 'stairs', label: 'Gerade Treppe',
@@ -299,6 +356,26 @@ function NeuesElementSection({ noGeschoss, activeName }) {
onClick: () => createTreppe({ treppeArt: 'wendel' }) }, onClick: () => createTreppe({ treppeArt: 'wendel' }) },
] ]
const profilItems = (factory) => [
{ icon: 'square', label: 'Quadrat',
hint: 'B × B', onClick: () => factory('quadrat') },
{ icon: 'rectangle', label: 'Rechteck',
hint: 'B × H', onClick: () => factory('rechteck') },
{ icon: 'circle', label: 'Rund',
hint: 'Durchmesser D', onClick: () => factory('rund') },
{ icon: 'view_column', label: 'I-Profil',
hint: 'Stahl HEB-Stil — Flansch B, Höhe H, Wand t',
onClick: () => factory('i_profil') },
{ icon: 'radio_button_unchecked', label: 'Rohr',
hint: 'Hohlzylinder — D Aussen, t Wanddicke',
onClick: () => factory('rohr') },
]
const stuetzeItems = profilItems((profil) =>
createStuetze({ profil }))
const traegerItems = profilItems((profil) =>
createTraeger({ profil }))
return ( return (
<div style={{ <div style={{
display: 'flex', flexDirection: 'column', gap: 10, display: 'flex', flexDirection: 'column', gap: 10,
@@ -334,22 +411,27 @@ function NeuesElementSection({ noGeschoss, activeName }) {
onClick={() => createDach({ geschoss: '' })} /> onClick={() => createDach({ geschoss: '' })} />
</PillGroup> </PillGroup>
<PillGroup label="Oeffnungen"> <PillGroup label="Öffnungen">
<PillButton icon="window" label="Fenster" <PillButton icon="window" label="Fenster"
hint={dis ? baseHint('Fenster') : hint={dis ? baseHint('Fenster') :
'Erst Wand-Achse waehlen, dann Punkt darauf'} disabled={dis} 'Erst Wand-Achse wählen, dann Punkt darauf'} disabled={dis}
onClick={() => createFenster({})} /> onClick={() => createFenster({})} />
<PillButton icon="sensor_door" label="Tuer" <PillButton icon="sensor_door" label="Tür"
hint={dis ? baseHint('Tuer') : hint={dis ? baseHint('Tür') :
'Erst Wand-Achse waehlen, dann Punkt darauf'} disabled={dis} 'Erst Wand-Achse wählen, dann Punkt darauf'} disabled={dis}
onClick={() => createTuer({})} /> onClick={() => createTuer({})} />
<PillButton icon="rectangle" label="Aussparung"
hint={dis ? baseHint('Aussparung') :
'Outline auf einer Decke zeichnen — wird automatisch ausgeschnitten'}
disabled={dis}
onClick={() => createAussparung({})} />
</PillGroup> </PillGroup>
<PillGroup label="Erschliessung"> <PillGroup label="Erschliessung">
<div ref={treppeWrapperRef} style={{ position: 'relative' }}> <div ref={treppeWrapperRef} style={{ position: 'relative' }}>
<PillButton icon="stairs" label="Treppe" hasMenu <PillButton icon="stairs" label="Treppe" hasMenu
hint={dis ? baseHint('Treppe') : hint={dis ? baseHint('Treppe') :
'Klick: gerade Treppe · Rechtsklick: Typ waehlen'} 'Klick: gerade Treppe · Rechtsklick: Typ wählen'}
disabled={dis} disabled={dis}
onClick={() => createTreppe({ treppeArt: 'gerade' })} onClick={() => createTreppe({ treppeArt: 'gerade' })}
onContextMenu={openTreppeMenu} /> onContextMenu={openTreppeMenu} />
@@ -359,6 +441,41 @@ function NeuesElementSection({ noGeschoss, activeName }) {
)} )}
</div> </div>
</PillGroup> </PillGroup>
<PillGroup label="Tragwerk">
<div style={{ position: 'relative' }}>
<PillButton icon="square_foot" label="Stütze" hasMenu
hint={dis ? baseHint('Stütze') :
'Klick: Quadrat-Stütze · Rechtsklick: Profil wählen'}
disabled={dis}
onClick={() => createStuetze({})}
onContextMenu={openStuetzeMenu} />
{stuetzeMenuOpen && (
<PopupMenu items={stuetzeItems}
onClose={() => setStuetzeMenuOpen(false)} />
)}
</div>
<div style={{ position: 'relative' }}>
<PillButton icon="horizontal_rule" label="Träger" hasMenu
hint={dis ? baseHint('Träger') :
'Klick: Rechteck-Träger · Rechtsklick: Profil wählen'}
disabled={dis}
onClick={() => createTraeger({})}
onContextMenu={openTraegerMenu} />
{traegerMenuOpen && (
<PopupMenu items={traegerItems}
onClose={() => setTraegerMenuOpen(false)} />
)}
</div>
</PillGroup>
<PillGroup label="Raeume">
<PillButton icon="crop_free" label="Raum"
hint={dis ? baseHint('Raum') :
'Outline zeichnen · Stempel zeigt Name + Fläche'}
disabled={dis}
onClick={() => createRaum({})} />
</PillGroup>
</div> </div>
) )
} }
@@ -398,11 +515,19 @@ export default function ElementeApp() {
}}> }}>
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em' }}>ELEMENTE</span> <span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em' }}>ELEMENTE</span>
<span className="chip" style={{ fontSize: 8 }}>{elements.length}</span> <span className="chip" style={{ fontSize: 8 }}>{elements.length}</span>
<button
onClick={() => exportRaeume()}
className="btn-icon-tonal"
disabled={!elements.some(e => e.kind === 'raum')}
title="Raumliste als CSV exportieren"
>
<Icon name="download" size={14} />
</button>
<button <button
onClick={() => regenerateAllElements()} onClick={() => regenerateAllElements()}
className="btn-icon-tonal" className="btn-icon-tonal"
disabled={elements.length === 0} disabled={elements.length === 0}
title="Alle Elemente neu generieren (z.B. nach Geschoss-Aenderung)" title="Alle Elemente neu generieren (z.B. nach Geschoss-Änderung)"
> >
<Icon name="sync" size={14} /> <Icon name="sync" size={14} />
</button> </button>
@@ -427,26 +552,42 @@ export default function ElementeApp() {
{selected ? ( {selected ? (
selected.kind === 'wand' ? ( selected.kind === 'wand' ? (
<WallProperties wall={selected} geschosse={geschosse} <WallProperties wall={selected} geschosse={geschosse}
materials={state.materials || []}
onUpdate={(p) => updateElement(selected.id, p)} onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { if (window.confirm('Wand loeschen?')) deleteElement(selected.id) }} /> onDelete={() => { if (window.confirm('Wand löschen?')) deleteElement(selected.id) }} />
) : selected.kind === 'decke' ? ( ) : selected.kind === 'decke' ? (
<DeckenProperties decke={selected} geschosse={geschosse} <DeckenProperties decke={selected} geschosse={geschosse}
onUpdate={(p) => updateElement(selected.id, p)} onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { if (window.confirm('Decke loeschen?')) deleteElement(selected.id) }} /> onDelete={() => { if (window.confirm('Decke löschen?')) deleteElement(selected.id) }} />
) : selected.kind === 'dach' ? ( ) : selected.kind === 'dach' ? (
<DachProperties dach={selected} geschosse={geschosse} <DachProperties dach={selected} geschosse={geschosse}
onUpdate={(p) => updateElement(selected.id, p)} onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { if (window.confirm('Dach loeschen?')) deleteElement(selected.id) }} /> onDelete={() => { if (window.confirm('Dach löschen?')) deleteElement(selected.id) }} />
) : selected.kind === 'treppe' ? ( ) : selected.kind === 'treppe' ? (
<TreppeProperties treppe={selected} geschosse={geschosse} <TreppeProperties treppe={selected} geschosse={geschosse}
onUpdate={(p) => updateElement(selected.id, p)} onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { if (window.confirm('Treppe loeschen?')) deleteElement(selected.id) }} /> onDelete={() => { if (window.confirm('Treppe löschen?')) deleteElement(selected.id) }} />
) : (selected.kind === 'stuetze' || selected.kind === 'traeger') ? (
<TragwerkProperties el={selected}
onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => {
const lbl = (KIND_META[selected.kind] || {}).label || 'Element'
if (window.confirm(`${lbl} löschen?`)) deleteElement(selected.id)
}} />
) : selected.kind === 'raum' ? (
<RaumProperties raum={selected} geschosse={geschosse}
hatchPatterns={state.hatchPatterns}
onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { if (window.confirm('Raum löschen?')) deleteElement(selected.id) }} />
) : selected.kind === 'aussparung' ? (
<AussparungProperties aussp={selected}
onDelete={() => { if (window.confirm('Aussparung löschen?')) deleteElement(selected.id) }} />
) : ( ) : (
<OeffnungProperties oeff={selected} <OeffnungProperties oeff={selected}
onUpdate={(p) => updateElement(selected.id, p)} onUpdate={(p) => updateElement(selected.id, p)}
onDelete={() => { onDelete={() => {
const label = selected.kind === 'fenster' ? 'Fenster' : 'Tuer' const label = selected.kind === 'fenster' ? 'Fenster' : 'Tür'
if (window.confirm(`${label} loeschen?`)) deleteElement(selected.id) if (window.confirm(`${label} löschen?`)) deleteElement(selected.id)
}} /> }} />
) )
) : ( ) : (
@@ -461,7 +602,7 @@ export default function ElementeApp() {
<Icon name="touch_app" size={24} style={{ color: 'var(--text-muted)', opacity: 0.5 }} /> <Icon name="touch_app" size={24} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
<div style={{ marginTop: 6 }}>Kein Element selektiert.</div> <div style={{ marginTop: 6 }}>Kein Element selektiert.</div>
<div style={{ marginTop: 4, fontSize: 10 }}> <div style={{ marginTop: 4, fontSize: 10 }}>
Eine Wand-Achse oder Decken-Outline in Rhino auswaehlen. Eine Wand-Achse oder Decken-Outline in Rhino auswählen.
</div> </div>
</div> </div>
)} )}
@@ -475,7 +616,328 @@ export default function ElementeApp() {
) )
} }
function WallProperties({ wall, geschosse, onUpdate, onDelete }) { function NumberField({ label, value, onCommit, width, step }) {
const [raw, setRaw] = useState(String(value))
useEffect(() => { setRaw(String(value)) }, [value])
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)',
width: width || 60 }}>{label}</span>
<input type="text" value={raw}
onChange={(e) => setRaw(e.target.value)}
onBlur={() => {
const v = parseFloat(raw)
if (!Number.isNaN(v) && v !== value) onCommit(v)
else setRaw(String(value))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
</div>
)
}
function TragwerkProperties({ el, onUpdate, onDelete }) {
const meta = KIND_META[el.kind] || { icon: 'square_foot', label: 'Element' }
const profil = el.profil || 'quadrat'
const profilOpts = ['quadrat', 'rechteck', 'rund', 'i_profil', 'rohr']
const isRound = (profil === 'rund' || profil === 'rohr')
const showH = (profil === 'rechteck' || profil === 'i_profil')
const showT = (profil === 'rohr' || profil === 'i_profil')
const isStuetze = (el.kind === 'stuetze')
const [zRaw, setZRaw] = useState(el.zOver || '')
useEffect(() => { setZRaw(el.zOver || '') }, [el.id, el.zOver])
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Icon name={meta.icon} size={13}
style={{ color: 'var(--accent)', marginRight: 6 }} />
<span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}>
{meta.label} · {el.geschossName}
</span>
<button className="btn-icon-sm btn-icon-danger" onClick={onDelete}
title="Löschen">
<Icon name="delete" size={12} />
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>
Profil
</span>
<select value={profil}
onChange={(e) => onUpdate({ profil: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
{profilOpts.map(p => (
<option key={p} value={p}>
{(PROFIL_META[p] || {}).label || p}
</option>
))}
</select>
</div>
{!isRound && (
<NumberField label="Breite" value={el.b}
onCommit={(v) => onUpdate({ b: v })} />
)}
{showH && (
<NumberField label="Höhe" value={el.h}
onCommit={(v) => onUpdate({ h: v })} />
)}
{isRound && (
<NumberField label="Durchm." value={el.d}
onCommit={(v) => onUpdate({ d: v })} />
)}
{showT && (
<NumberField label="Wanddicke" value={el.t}
onCommit={(v) => onUpdate({ t: v })} />
)}
<NumberField label="Drehung°" value={el.angle}
onCommit={(v) => onUpdate({ angle: v })} />
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>
{isStuetze ? 'Höhe' : 'OK über UK'}
</span>
<input type="text" value={zRaw}
placeholder={`auto (${fmtNum(el.ok - el.uk)} m)`}
onChange={(e) => setZRaw(e.target.value)}
onBlur={() => {
const v = zRaw.trim()
if (v === '') {
if ((el.zOver || '') !== '') onUpdate({ zOver: '' })
} else {
const n = parseFloat(v)
if (!Number.isNaN(n)) onUpdate({ zOver: n })
}
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11,
fontFamily: 'DM Mono, monospace' }} />
</div>
<div style={{
display: 'flex', justifyContent: 'space-between',
fontSize: 10, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace',
paddingTop: 4, borderTop: '1px dashed var(--border)',
}}>
<span>UK {fmtNum(el.uk)} · OK {fmtNum(el.ok)}</span>
{!isStuetze && <span>L {fmtNum(el.axisLen)} m</span>}
</div>
</div>
)
}
function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns }) {
const [name, setName] = useState(raum.name || 'Raum')
const [nummer, setNummer] = useState(raum.nummer || '')
const [txtH, setTxtH] = useState(String(raum.txtH || 0.20))
useEffect(() => {
setName(raum.name || 'Raum')
setNummer(raum.nummer || '')
setTxtH(String(raum.txtH || 0.20))
}, [raum.id, raum.name, raum.nummer, raum.txtH])
// Aktueller Wert von raum_fuellung: "" | "Solid" | "Hatch1" | | "ByLayer"
const fuell = raum.fuellung || ''
const patternList = hatchPatterns || []
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Icon name="crop_free" size={13}
style={{ color: 'var(--accent)', marginRight: 6 }} />
<span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}>
Raum · {raum.geschossName}
</span>
<button className="btn-icon-sm btn-icon-danger" onClick={onDelete}
title="Löschen">
<Icon name="delete" size={12} />
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Geschoss</span>
<select value={raum.geschoss}
onChange={(e) => onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
{geschosse.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
</select>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Nummer</span>
<input type="text" value={nummer}
onChange={(e) => setNummer(e.target.value)}
onBlur={() => {
if (nummer !== (raum.nummer || '')) onUpdate({ nummer })
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11,
fontFamily: 'DM Mono, monospace' }} />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Name</span>
<input type="text" value={name}
onChange={(e) => setName(e.target.value)}
onBlur={() => {
const v = (name || 'Raum').trim()
if (v !== raum.name) onUpdate({ name: v })
else setName(v)
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11 }} />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Typ</span>
<select value={raum.sia || ''}
onChange={(e) => onUpdate({ sia: e.target.value })}
title="SIA 416 Flaechenklassifikation"
style={{ flex: 1, fontSize: 11 }}>
{RAUM_SIA_KINDS.map(s => (
<option key={s.code} value={s.code}>
{s.code === '' ? '—'
: `${s.label}${s.hint}`}
</option>
))}
</select>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Füllung</span>
<select value={fuell}
onChange={(e) => onUpdate({ fuellung: e.target.value })}
title="Hatch-Pattern im Normalmodus. Bei aktivem SIA-Modus wird klassifizierten Raeumen automatisch Solid forciert."
style={{ flex: 1, fontSize: 11 }}>
<option value="">Keine</option>
<option value="ByLayer">Ebene (folgt Layer-Farbe)</option>
{patternList.length > 0 && <option disabled></option>}
{patternList.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Rundung</span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
{RAUM_RUNDUNGEN.map(r => (
<button key={r}
onClick={() => onUpdate({ rundung: r })}
className={raum.rundung === r ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 4px', fontSize: 10 }}
title={r === 'exakt' ? '2 Nachkommastellen, ohne Rundung'
: r === '0.01' ? 'auf 0.01 m²'
: r === '0.1' ? 'auf 0.1 m²'
: r === '0.5' ? 'auf 0.5 m²'
: 'auf ganze m²'}>
{r}
</button>
))}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Ausrichtung</span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
{RAUM_ALIGN.map(a => (
<button key={a.code}
onClick={() => onUpdate({ align: a.code })}
className={(raum.align || 'mid') === a.code ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 4px', fontSize: 10,
display: 'flex', alignItems: 'center',
justifyContent: 'center', gap: 3 }}
title={a.label}>
<Icon name={a.icon} size={12} />
</button>
))}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Texthöhe</span>
<input type="text" value={txtH}
onChange={(e) => setTxtH(e.target.value)}
onBlur={() => {
const v = parseFloat(txtH)
if (v > 0 && v !== raum.txtH) onUpdate({ txtH: v })
else setTxtH(String(raum.txtH))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11,
fontFamily: 'DM Mono, monospace' }} />
</div>
<div style={{
display: 'flex', justifyContent: 'space-between',
fontSize: 10, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace',
paddingTop: 4, borderTop: '1px dashed var(--border)',
}}>
<span>Flaeche: <strong style={{ color: 'var(--text-primary)' }}>
{raum.areaFmt} </strong></span>
<span>Umfang: {fmtNum(raum.umfang)} m</span>
</div>
</div>
)
}
function AussparungProperties({ aussp, onDelete }) {
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Icon name="rectangle" size={13}
style={{ color: 'var(--accent)', marginRight: 6 }} />
<span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}>
Aussparung · {aussp.geschossName}
</span>
<button className="btn-icon-sm btn-icon-danger" onClick={onDelete}
title="Löschen">
<Icon name="delete" size={12} />
</button>
</div>
<div style={{
display: 'flex', justifyContent: 'space-between',
fontSize: 10, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace',
paddingTop: 4, borderTop: '1px dashed var(--border)',
}}>
<span>Fläche: {fmtNum(aussp.area)} </span>
<span>Umfang: {fmtNum(aussp.umfang)} m</span>
</div>
<div style={{
fontSize: 9, color: 'var(--text-muted)', fontStyle: 'italic',
lineHeight: 1.4,
}}>
Outline in Rhino editieren (Punkte ziehen, _Reshape, ) die
Eltern-Decke wird automatisch nachgerechnet.
</div>
</div>
)
}
function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
const [dicke, setDicke] = useState(String(wall.dicke)) const [dicke, setDicke] = useState(String(wall.dicke))
const [ukOver, setUkOver] = useState(wall.ukOverride) const [ukOver, setUkOver] = useState(wall.ukOverride)
const [okOver, setOkOver] = useState(wall.okOverride) const [okOver, setOkOver] = useState(wall.okOverride)
@@ -501,7 +963,7 @@ function WallProperties({ wall, geschosse, onUpdate, onDelete }) {
<span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}> <span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}>
Wand · {wall.geschossName} Wand · {wall.geschossName}
</span> </span>
<button className="btn-icon-sm btn-icon-danger" onClick={onDelete} title="Loeschen"> <button className="btn-icon-sm btn-icon-danger" onClick={onDelete} title="Löschen">
<Icon name="delete" size={12} /> <Icon name="delete" size={12} />
</button> </button>
</div> </div>
@@ -515,6 +977,24 @@ function WallProperties({ wall, geschosse, onUpdate, onDelete }) {
</select> </select>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Aufbau</span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
<button
onClick={() => onUpdate({ layered: false })}
className={!wall.layered ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title="Eine homogene Wand-Schicht (Standard)">Solid</button>
<button
onClick={() => onUpdate({ layered: true })}
className={wall.layered ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title="Mehrere Schichten mit individuellen Dicken und Farben"
>Mehrschichtig</button>
</div>
</div>
{!wall.layered && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Dicke</span> <span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Dicke</span>
<input type="text" value={dicke} <input type="text" value={dicke}
@@ -527,6 +1007,13 @@ function WallProperties({ wall, geschosse, onUpdate, onDelete }) {
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
</div> </div>
)}
{wall.layered && (
<LayersEditor layers={wall.layers || []}
materials={materials}
onChange={(layers) => onUpdate({ layers })} />
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Referenz</span> <span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Referenz</span>
@@ -555,6 +1042,120 @@ function WallProperties({ wall, geschosse, onUpdate, onDelete }) {
) )
} }
function LayersEditor({ layers, onChange, materials }) {
const total = (layers || []).reduce((s, l) => s + (parseFloat(l.dicke) || 0), 0)
const updateAt = (idx, patch) => {
const next = (layers || []).map((l, i) => (i === idx ? { ...l, ...patch } : l))
onChange(next)
}
const removeAt = (idx) => {
const next = (layers || []).filter((_, i) => i !== idx)
onChange(next.length ? next : [{ name: 'Schicht 1', dicke: 0.20, color: '#cccccc', material: '' }])
}
const addLayer = () => {
const idx = (layers || []).length + 1
onChange([ ...(layers || []),
{ name: `Schicht ${idx}`, dicke: 0.05, color: '#dddddd', material: '' } ])
}
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 8px',
background: 'var(--bg-input)',
border: '1px solid var(--border-light)',
borderRadius: 'var(--r)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4,
fontSize: 9, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase',
fontWeight: 600 }}>
<span style={{ flex: 1 }}>Schichten (links rechts)</span>
<span style={{ fontFamily: 'DM Mono, monospace', fontSize: 10,
color: 'var(--text-primary)', textTransform: 'none' }}>
Σ {total.toFixed(3)} m
</span>
</div>
{(layers || []).map((ly, i) => (
<LayerRow key={i} layer={ly}
materials={materials}
onChange={(patch) => updateAt(i, patch)}
onRemove={() => removeAt(i)} />
))}
<button onClick={addLayer} className="btn-outlined"
style={{ padding: '3px 6px', fontSize: 10, marginTop: 2 }}
title="Neue Schicht unten anfügen">
+ Schicht
</button>
</div>
)
}
function LayerRow({ layer, materials, onChange, onRemove }) {
const [name, setName] = useState(layer.name || '')
const [dicke, setDicke] = useState(String(layer.dicke || 0))
useEffect(() => {
setName(layer.name || '')
setDicke(String(layer.dicke || 0))
}, [layer.name, layer.dicke])
// Wenn ein Material gewaehlt ist, kommt die Farbe von dort der Color-
// Picker wird ausgegraut und zeigt die Material-Farbe an.
const matList = materials || []
const matName = layer.material || ''
const matDef = matList.find(m => m.name === matName)
const effectiveColor = matDef ? matDef.color : (layer.color || '#cccccc')
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<input type="color"
value={effectiveColor}
onChange={(e) => onChange({ color: e.target.value })}
disabled={!!matDef}
title={matDef
? `Farbe aus Material "${matDef.name}"`
: 'Schicht-Farbe'}
style={{
width: 22, height: 22, padding: 0, border: 'none',
background: 'transparent',
cursor: matDef ? 'not-allowed' : 'pointer',
opacity: matDef ? 0.5 : 1,
}} />
<select value={matName}
onChange={(e) => {
const m = matList.find(x => x.name === e.target.value)
if (m) onChange({ material: m.name, color: m.color })
else onChange({ material: '' })
}}
title="Material aus Bibliothek (steuert Farbe + Section-Hatch)"
style={{ width: 92, fontSize: 10, padding: '2px 4px' }}>
<option value="">(eigene)</option>
{matList.map(m => (
<option key={m.name} value={m.name}>{m.name}</option>
))}
</select>
<input type="text" value={name}
placeholder="Name"
onChange={(e) => setName(e.target.value)}
onBlur={() => { if (name !== layer.name) onChange({ name }) }}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 10, padding: '2px 4px' }} />
<input type="text" value={dicke}
onChange={(e) => setDicke(e.target.value)}
onBlur={() => {
const v = parseFloat(dicke)
if (v > 0 && v !== layer.dicke) onChange({ dicke: v })
else setDicke(String(layer.dicke))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ width: 56, fontSize: 10, padding: '2px 4px',
fontFamily: 'DM Mono, monospace' }}
title="Dicke in Metern" />
<button onClick={onRemove} className="btn-icon-sm btn-icon-danger"
title="Schicht entfernen" style={{ width: 18, height: 18 }}>
<Icon name="close" size={10} />
</button>
</div>
)
}
function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) { function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
const [dicke, setDicke] = useState(String(decke.dicke)) const [dicke, setDicke] = useState(String(decke.dicke))
const [ukOver, setUkOver] = useState(decke.ukOverride) const [ukOver, setUkOver] = useState(decke.ukOverride)
@@ -581,7 +1182,7 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
<span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}> <span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}>
Decke · {decke.geschossName} Decke · {decke.geschossName}
</span> </span>
<button className="btn-icon-sm btn-icon-danger" onClick={onDelete} title="Loeschen"> <button className="btn-icon-sm btn-icon-danger" onClick={onDelete} title="Löschen">
<Icon name="delete" size={12} /> <Icon name="delete" size={12} />
</button> </button>
</div> </div>
@@ -657,7 +1258,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
<span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}> <span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}>
{dachTyp === 'sattel' ? 'Satteldach' : dachTyp === 'walm' ? 'Walmdach' : 'Pultdach'} · {dach.geschossName} {dachTyp === 'sattel' ? 'Satteldach' : dachTyp === 'walm' ? 'Walmdach' : 'Pultdach'} · {dach.geschossName}
</span> </span>
<button className="btn-icon-sm btn-icon-danger" onClick={onDelete} title="Loeschen"> <button className="btn-icon-sm btn-icon-danger" onClick={onDelete} title="Löschen">
<Icon name="delete" size={12} /> <Icon name="delete" size={12} />
</button> </button>
</div> </div>
@@ -677,7 +1278,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
className={dachTyp === o.code ? 'btn-contained' : 'btn-outlined'} className={dachTyp === o.code ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 4px', fontSize: 9 }} style={{ flex: 1, padding: '3px 4px', fontSize: 9 }}
title={o.code === 'pult' title={o.code === 'pult'
? 'Geneigte Flaeche, Traufe = 1. Kante' ? 'Geneigte Fläche, Traufe = 1. Kante'
: 'Erfordert Rechteck-Outline (4 Ecken)'}> : 'Erfordert Rechteck-Outline (4 Ecken)'}>
{o.label} {o.label}
</button> </button>
@@ -749,7 +1350,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
{[ {[
{ code: 'walm', label: 'Walm', hint: 'Knick auf allen 4 Seiten (Pariser Stil)' }, { code: 'walm', label: 'Walm', hint: 'Knick auf allen 4 Seiten (Pariser Stil)' },
{ code: 'giebel', label: 'Giebel', hint: 'Knick nur an langen Seiten, Schmalseiten als Giebelwand (DACH-Standard)' }, { code: 'giebel', label: 'Giebel', hint: 'Knick nur an langen Seiten, Schmalseiten als Giebelwand (DACH-Standard)' },
{ code: 'walm_giebel', label: 'W-G', hint: 'Unten Walm (Knick rundum), oben Giebel mit First ueber voller Laenge' }, { code: 'walm_giebel', label: 'W-G', hint: 'Unten Walm (Knick rundum), oben Giebel mit First über voller Länge' },
].map(o => ( ].map(o => (
<button key={o.code} <button key={o.code}
onClick={() => onUpdate({ dachVariante: o.code })} onClick={() => onUpdate({ dachVariante: o.code })}
@@ -804,7 +1405,7 @@ function MansardeFields({ dach, onUpdate }) {
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }} <span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
title="Hoehe ueber Traufe wo der Knick sitzt"> title="Höhe über Traufe wo der Knick sitzt">
Knick H Knick H
</span> </span>
<input type="text" value={kh} <input type="text" value={kh}
@@ -830,15 +1431,15 @@ const SIMS_OPTIONS = [
] ]
const RAHMEN_POS_OPTIONS = [ const RAHMEN_POS_OPTIONS = [
{ code: 'aussen', label: 'aussen', hint: 'Rahmen buendig mit Aussenflaeche' }, { code: 'aussen', label: 'aussen', hint: 'Rahmen bündig mit Aussenfläche' },
{ code: 'mid', label: 'mittig', hint: 'Rahmen mittig im Wandquerschnitt' }, { code: 'mid', label: 'mittig', hint: 'Rahmen mittig im Wandquerschnitt' },
{ code: 'innen', label: 'innen', hint: 'Rahmen buendig mit Innenflaeche' }, { code: 'innen', label: 'innen', hint: 'Rahmen bündig mit Innenfläche' },
] ]
const OEFF_REFERENZ_OPTIONS = [ const OEFF_REFERENZ_OPTIONS = [
{ code: 'links', label: 'Links', hint: 'Klick-Punkt am linken Rand — Oeffnung extendiert nach rechts (+tan der Wand-Achse)' }, { code: 'links', label: 'Links', hint: 'Klick-Punkt am linken Rand — Öffnung extendiert nach rechts (+tan der Wand-Achse)' },
{ code: 'mid', label: 'Mittig', hint: 'Klick-Punkt mittig in der Oeffnung (Standard)' }, { code: 'mid', label: 'Mittig', hint: 'Klick-Punkt mittig in der Öffnung (Standard)' },
{ code: 'rechts', label: 'Rechts', hint: 'Klick-Punkt am rechten Rand — Oeffnung extendiert nach links (-tan)' }, { code: 'rechts', label: 'Rechts', hint: 'Klick-Punkt am rechten Rand — Öffnung extendiert nach links (-tan)' },
] ]
function SollRow({ label, value, unit, soll, sollKey, onUpdateSoll, readOnly }) { function SollRow({ label, value, unit, soll, sollKey, onUpdateSoll, readOnly }) {
@@ -976,7 +1577,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
const modus = treppe.treppeModus ?? 'flach' const modus = treppe.treppeModus ?? 'flach'
const MODUS_OPTIONS = [ const MODUS_OPTIONS = [
{ code: 'massiv', label: 'massiv', hint: 'Block bis zum Boden — wie eine Mauer unter der Treppe' }, { code: 'massiv', label: 'massiv', hint: 'Block bis zum Boden — wie eine Mauer unter der Treppe' },
{ code: 'flach', label: 'flach', hint: 'Schraege Plattenunterseite parallel zum Treppenlauf (realistisch)' }, { code: 'flach', label: 'flach', hint: 'Schräge Plattenunterseite parallel zum Treppenlauf (realistisch)' },
{ code: 'plattenrand', label: 'gestuft', hint: 'Plattenunterseite folgt den Stufen, vertikal versetzt' }, { code: 'plattenrand', label: 'gestuft', hint: 'Plattenunterseite folgt den Stufen, vertikal versetzt' },
] ]
@@ -993,7 +1594,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
<span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}> <span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}>
Treppe · {treppe.geschossName} {treppe.geschossEndName || '(auto)'} Treppe · {treppe.geschossName} {treppe.geschossEndName || '(auto)'}
</span> </span>
<button className="btn-icon-sm btn-icon-danger" onClick={onDelete} title="Loeschen"> <button className="btn-icon-sm btn-icon-danger" onClick={onDelete} title="Löschen">
<Icon name="delete" size={12} /> <Icon name="delete" size={12} />
</button> </button>
</div> </div>
@@ -1021,10 +1622,10 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
} }
}} }}
style={{ flex: 1, fontSize: 11 }}> style={{ flex: 1, fontSize: 11 }}>
<option value="">(auto: Start + Hoehe)</option> <option value="">(auto: Start + Höhe)</option>
{geschosse.filter(g => g.id !== treppe.geschoss) {geschosse.filter(g => g.id !== treppe.geschoss)
.map(g => <option key={g.id} value={g.id}>{g.name}</option>)} .map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
<option value="__custom__">eigene Hoehe</option> <option value="__custom__">eigene Höhe</option>
</select> </select>
</div> </div>
@@ -1136,7 +1737,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
<span style={{ color: 'var(--text-muted)' }}>m</span> <span style={{ color: 'var(--text-muted)' }}>m</span>
{hasHOver && ( {hasHOver && (
<button onClick={onClearHOver} <button onClick={onClearHOver}
title="Zurueck zu Geschoss-Hoehe" title="Zurück zu Geschoss-Höhe"
style={{ style={{
marginLeft: 'auto', fontSize: 9, marginLeft: 'auto', fontSize: 9,
background: 'transparent', border: 'none', background: 'transparent', border: 'none',
@@ -1158,7 +1759,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
function OeffnungProperties({ oeff, onUpdate, onDelete }) { function OeffnungProperties({ oeff, onUpdate, onDelete }) {
const isFenster = oeff.kind === 'fenster' const isFenster = oeff.kind === 'fenster'
const label = isFenster ? 'Fenster' : 'Tuer' const label = isFenster ? 'Fenster' : 'Tür'
const icon = isFenster ? 'window' : 'sensor_door' const icon = isFenster ? 'window' : 'sensor_door'
const [breite, setBreite] = useState(String(oeff.breite ?? (isFenster ? 1.2 : 0.9))) const [breite, setBreite] = useState(String(oeff.breite ?? (isFenster ? 1.2 : 0.9)))
const [hoehe, setHoehe] = useState(String(oeff.hoehe ?? (isFenster ? 1.4 : 2.1))) const [hoehe, setHoehe] = useState(String(oeff.hoehe ?? (isFenster ? 1.4 : 2.1)))
@@ -1198,7 +1799,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
<span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}> <span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}>
{label} · {oeff.geschossName} {label} · {oeff.geschossName}
</span> </span>
<button className="btn-icon-sm btn-icon-danger" onClick={onDelete} title="Loeschen"> <button className="btn-icon-sm btn-icon-danger" onClick={onDelete} title="Löschen">
<Icon name="delete" size={12} /> <Icon name="delete" size={12} />
</button> </button>
</div> </div>
@@ -1214,7 +1815,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Hoehe</span> <span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Höhe</span>
<input type="text" value={hoehe} <input type="text" value={hoehe}
onChange={(e) => setHoehe(e.target.value)} onChange={(e) => setHoehe(e.target.value)}
onBlur={() => commit('hoehe', hoehe, setHoehe, oeff.hoehe)} onBlur={() => commit('hoehe', hoehe, setHoehe, oeff.hoehe)}
@@ -1223,11 +1824,12 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span> <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
</div> </div>
{isFenster && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }} <span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
title="Bruestungshoehe ueber UK der Wand"> title={isFenster
Bruest. ? 'Brüstungshöhe über UK der Wand'
: 'Türschwelle / Höhe über UK der Wand'}>
{isFenster ? 'Brüst.' : 'Schw.'}
</span> </span>
<input type="text" value={brueest} <input type="text" value={brueest}
onChange={(e) => setBrueest(e.target.value)} onChange={(e) => setBrueest(e.target.value)}
@@ -1240,12 +1842,11 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span> <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
</div> </div>
)}
{/* Referenz-Lage: wo sitzt der Klick-Punkt in der Oeffnung */} {/* Referenz-Lage: wo sitzt der Klick-Punkt in der Oeffnung */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }} <span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
title="Lage des Klick-Punkts in der Oeffnung"> title="Lage des Klick-Punkts in der Öffnung">
Ref Ref
</span> </span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}> <div style={{ flex: 1, display: 'flex', gap: 2 }}>
@@ -1304,11 +1905,12 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</div> </div>
</div> </div>
{/* Fluegel-Anzahl */} {/* Fluegel-Anzahl — nur fuer Fenster (Tueren haben ein einzelnes Tuerblatt) */}
{isFenster && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }} <span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
title="Anzahl Fluegel (vertikale Unterteilung)"> title="Anzahl Flügel (vertikale Unterteilung)">
Fluegel Flügel
</span> </span>
<div style={{ flex: 1, display: 'flex', gap: 2 }}> <div style={{ flex: 1, display: 'flex', gap: 2 }}>
{[1, 2, 3, 4].map(n => ( {[1, 2, 3, 4].map(n => (
@@ -1321,13 +1923,14 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
))} ))}
</div> </div>
</div> </div>
)}
{/* Sims-Stile (aussen / innen) — nur fuer Fenster */} {/* Sims-Stile (aussen / innen) — nur fuer Fenster */}
{isFenster && ( {isFenster && (
<> <>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }} <span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
title="Aussensims — Platte unter Oeffnung, ragt aussen heraus"> title="Aussensims — Platte unter Öffnung, ragt aussen heraus">
Sims a. Sims a.
</span> </span>
<select value={simsAus} <select value={simsAus}
@@ -1339,7 +1942,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }} <span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
title="Innensims — Platte unter Oeffnung, ragt innen heraus"> title="Innensims — Platte unter Öffnung, ragt innen heraus">
Sims i. Sims i.
</span> </span>
<select value={simsIn} <select value={simsIn}
@@ -1358,7 +1961,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
onClick={() => onUpdate({ glas: !oeff.glas })} onClick={() => onUpdate({ glas: !oeff.glas })}
className={oeff.glas ? 'btn-contained' : 'btn-outlined'} className={oeff.glas ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }} style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title={isFenster ? 'Glasscheibe sichtbar' : 'Verglaste Tuer (statt Tuerblatt)'}> title={isFenster ? 'Glasscheibe sichtbar' : 'Verglaste Tür (statt Türblatt)'}>
{isFenster ? 'Glas' : 'Verglast'} {oeff.glas ? '✓' : ''} {isFenster ? 'Glas' : 'Verglast'} {oeff.glas ? '✓' : ''}
</button> </button>
</div> </div>
+1 -1
View File
@@ -217,7 +217,7 @@ function PenLw({ sel }) {
<button <button
className="btn-icon-sm" className="btn-icon-sm"
onClick={() => setLwSource(source === 'object' ? 'layer' : 'object', source === 'object' ? null : effective)} onClick={() => setLwSource(source === 'object' ? 'layer' : 'object', source === 'object' ? null : effective)}
title={source === 'object' ? 'Nach Ebene' : 'Uebersteuern'} title={source === 'object' ? 'Nach Ebene' : 'Übersteuern'}
style={{ color: source === 'layer' ? 'var(--text-primary)' : 'var(--text-muted)' }} style={{ color: source === 'layer' ? 'var(--text-primary)' : 'var(--text-muted)' }}
> >
<Icon name={source === 'layer' ? 'link' : 'link_off'} size={14} /> <Icon name={source === 'layer' ? 'link' : 'link_off'} size={14} />
+5 -5
View File
@@ -203,9 +203,9 @@ export default function LayoutsApp() {
onClick: () => setLayoutFolder(l.id, '') }, onClick: () => setLayoutFolder(l.id, '') },
{ divider: true }, { divider: true },
] : []), ] : []),
{ label: 'Loeschen', icon: 'delete', danger: true, { label: 'Löschen', icon: 'delete', danger: true,
onClick: () => { onClick: () => {
if (window.confirm(`Layout "${l.name}" loeschen?`)) deleteLayout(l.id) if (window.confirm(`Layout "${l.name}" löschen?`)) deleteLayout(l.id)
} }, } },
] ]
@@ -217,7 +217,7 @@ export default function LayoutsApp() {
icon: collapsedFolders.has(folderName) ? 'expand_more' : 'expand_less', icon: collapsedFolders.has(folderName) ? 'expand_more' : 'expand_less',
onClick: () => toggleFolderCollapse(folderName) }, onClick: () => toggleFolderCollapse(folderName) },
{ divider: true }, { divider: true },
{ label: 'Alle ankreuzen / abwaehlen', { label: 'Alle ankreuzen / abwählen',
icon: 'check_box', icon: 'check_box',
onClick: () => checkAllInFolder(items) }, onClick: () => checkAllInFolder(items) },
{ label: `Ordner als PDF (${items.length})`, { label: `Ordner als PDF (${items.length})`,
@@ -545,7 +545,7 @@ export default function LayoutsApp() {
<Icon name="crop_landscape" size={24} style={{ color: 'var(--text-muted)', opacity: 0.5 }} /> <Icon name="crop_landscape" size={24} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
<div style={{ marginTop: 6 }}>Keine Details auf diesem Layout.</div> <div style={{ marginTop: 6 }}>Keine Details auf diesem Layout.</div>
<div style={{ marginTop: 4, fontSize: 10 }}> <div style={{ marginTop: 4, fontSize: 10 }}>
Oben <Icon name="add" size={11} /> klicken um eines hinzuzufuegen. Oben <Icon name="add" size={11} /> klicken um eines hinzuzufügen.
</div> </div>
</div> </div>
) : ( ) : (
@@ -723,7 +723,7 @@ function LayoutDialog({ mode, layout, onCancel, onSubmit }) {
style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} /> style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} />
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>×</span> <span style={{ color: 'var(--text-muted)', fontSize: 10 }}>×</span>
<input type="text" value={ch} onChange={(e) => setCh(e.target.value)} <input type="text" value={ch} onChange={(e) => setCh(e.target.value)}
placeholder="Hoehe" placeholder="Höhe"
style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} /> style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} />
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>mm</span> <span style={{ color: 'var(--text-muted)', fontSize: 10 }}>mm</span>
</div> </div>
+6 -6
View File
@@ -168,7 +168,7 @@ export default function MassstabApp() {
value={dropdownValue} value={dropdownValue}
onChange={(e) => applyDropdown(e.target.value)} onChange={(e) => applyDropdown(e.target.value)}
style={{ ...cellInput, width: 80 }} style={{ ...cellInput, width: 80 }}
title="Massstab waehlen" title="Massstab wählen"
> >
<option value="__none__">1:?</option> <option value="__none__">1:?</option>
{PRESETS.map(p => ( {PRESETS.map(p => (
@@ -201,7 +201,7 @@ export default function MassstabApp() {
style={cellBtn} style={cellBtn}
title={appliedScale title={appliedScale
? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})` ? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})`
: 'Erst einen Massstab waehlen'} : 'Erst einen Massstab wählen'}
>100%</button> >100%</button>
<button onClick={zoomExtents} style={cellBtn} title="Auf gesamten Inhalt zoomen"> <button onClick={zoomExtents} style={cellBtn} title="Auf gesamten Inhalt zoomen">
<Icon name="fit_screen" size={14} /> <Icon name="fit_screen" size={14} />
@@ -224,8 +224,8 @@ export default function MassstabApp() {
borderColor: state.showLineweights ? 'var(--accent)' : 'var(--border)', borderColor: state.showLineweights ? 'var(--accent)' : 'var(--border)',
}} }}
title={state.showLineweights title={state.showLineweights
? 'Strichstaerken werden angezeigt (Print-View) — klicken zum Ausschalten' ? 'Strichstärken werden angezeigt (Print-View) — klicken zum Ausschalten'
: 'Strichstaerken als Hairlines (Edit-View) — klicken um Print-View zu zeigen'} : 'Strichstärken als Hairlines (Edit-View) — klicken um Print-View zu zeigen'}
> >
<Icon name="edit" size={14} style={{ display: state.showLineweights ? 'none' : 'inline-block' }} /> <Icon name="edit" size={14} style={{ display: state.showLineweights ? 'none' : 'inline-block' }} />
<Icon name="print" size={14} style={{ display: state.showLineweights ? 'inline-block' : 'none' }} /> <Icon name="print" size={14} style={{ display: state.showLineweights ? 'inline-block' : 'none' }} />
@@ -268,7 +268,7 @@ export default function MassstabApp() {
boxShadow: '0 4px 12px rgba(0,0,0,0.15)', zIndex: 10, minWidth: 220, boxShadow: '0 4px 12px rgba(0,0,0,0.15)', zIndex: 10, minWidth: 220,
}}> }}>
<div style={{ fontSize: 10, color: 'var(--text-muted)' }}> <div style={{ fontSize: 10, color: 'var(--text-muted)' }}>
Bildschirm-Aufloesung fuer Massstab-Berechnung Bildschirm-Auflösung für Massstab-Berechnung
</div> </div>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input <input
@@ -284,7 +284,7 @@ export default function MassstabApp() {
<button <button
onClick={() => { detectMassstabDpi(); setDpiOpen(false) }} onClick={() => { detectMassstabDpi(); setDpiOpen(false) }}
style={{ ...cellBtn, fontSize: 10, justifyContent: 'flex-start' }} style={{ ...cellBtn, fontSize: 10, justifyContent: 'flex-start' }}
title="DPI automatisch ueber EDID des Bildschirms ermitteln" title="DPI automatisch über EDID des Bildschirms ermitteln"
> >
<Icon name="auto_fix_high" size={12} /> Auto-Detect (EDID) <Icon name="auto_fix_high" size={12} /> Auto-Detect (EDID)
</button> </button>
+16 -2
View File
@@ -7,6 +7,7 @@ import {
setMassstabDpi, detectMassstabDpi, setMassstabDpi, detectMassstabDpi,
setView, setDisplayMode, setView, setDisplayMode,
toggleOverrides, setOverridesPreset, openOverridesPanel, toggleOverrides, setOverridesPreset, openOverridesPanel,
openDossierSettings,
} from './lib/rhinoBridge' } from './lib/rhinoBridge'
const PRESETS = [ const PRESETS = [
@@ -218,6 +219,19 @@ export default function OberleisteApp() {
> >
DOSSIER <span style={{ opacity: 0.55 }}>{__APP_VERSION__}</span> DOSSIER <span style={{ opacity: 0.55 }}>{__APP_VERSION__}</span>
</span> </span>
<button
onClick={() => openDossierSettings()}
title="Dossier-Einstellungen"
style={{
background: 'transparent', border: 'none', padding: '2px 4px',
cursor: 'pointer', color: 'var(--text-muted)',
display: 'flex', alignItems: 'center', flexShrink: 0,
}}
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--text-primary)'}
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}
>
<Icon name="settings" size={14} />
</button>
<div style={sep} /> <div style={sep} />
{/* ====== GRUPPE: VIEW ====== */} {/* ====== GRUPPE: VIEW ====== */}
<span style={groupLabel}>View</span> <span style={groupLabel}>View</span>
@@ -302,7 +316,7 @@ export default function OberleisteApp() {
onClick={apply100} onClick={apply100}
disabled={isPerspective || !appliedScale} disabled={isPerspective || !appliedScale}
label="100%" label="100%"
title={appliedScale ? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})` : 'Erst einen Massstab waehlen'} title={appliedScale ? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})` : 'Erst einen Massstab wählen'}
/> />
<button className="btn-icon" onClick={zoomExtents} <button className="btn-icon" onClick={zoomExtents}
style={pillIconBtn} style={pillIconBtn}
@@ -318,7 +332,7 @@ export default function OberleisteApp() {
onClick={() => setShowLineweights(!state.showLineweights)} onClick={() => setShowLineweights(!state.showLineweights)}
active={state.showLineweights} active={state.showLineweights}
label={state.showLineweights ? 'Print' : 'Edit'} label={state.showLineweights ? 'Print' : 'Edit'}
title={state.showLineweights ? 'Print-View aktiv — klick zum Ausschalten' : 'Strichstaerken anzeigen (Print-View)'} title={state.showLineweights ? 'Print-View aktiv — klick zum Ausschalten' : 'Strichstärken anzeigen (Print-View)'}
icon={state.showLineweights ? 'print' : 'edit'} icon={state.showLineweights ? 'print' : 'edit'}
/> />
+5 -5
View File
@@ -436,10 +436,10 @@ export default function OverridesApp() {
icon: 'content_copy', icon: 'content_copy',
onClick: () => duplicateRule(ruleId) }, onClick: () => duplicateRule(ruleId) },
{ divider: true }, { divider: true },
{ label: 'Loeschen', { label: 'Löschen',
icon: 'delete', danger: true, icon: 'delete', danger: true,
onClick: () => { onClick: () => {
if (window.confirm(`Regel "${rule.name || '(ohne Name)'}" loeschen?`)) deleteRule(ruleId) if (window.confirm(`Regel "${rule.name || '(ohne Name)'}" löschen?`)) deleteRule(ruleId)
} }, } },
] ]
} }
@@ -516,10 +516,10 @@ export default function OverridesApp() {
if (selectedPreset) { savePreset(selectedPreset); return } if (selectedPreset) { savePreset(selectedPreset); return }
const existing = (state.presets || []).map(p => p.name) const existing = (state.presets || []).map(p => p.name)
const def = `Kombination ${existing.length + 1}` const def = `Kombination ${existing.length + 1}`
const name = window.prompt('Name fuer neue Kombination:', def) const name = window.prompt('Name für neue Kombination:', def)
if (!name || !name.trim()) return if (!name || !name.trim()) return
const t = name.trim() const t = name.trim()
if (existing.includes(t) && !window.confirm(`Kombination "${t}" ueberschreiben?`)) return if (existing.includes(t) && !window.confirm(`Kombination "${t}" überschreiben?`)) return
savePreset(t) savePreset(t)
setSelectedPreset(t) setSelectedPreset(t)
}} }}
@@ -527,7 +527,7 @@ export default function OverridesApp() {
className="btn-outlined" className="btn-outlined"
style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }} style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
title={selectedPreset title={selectedPreset
? `Aenderungen in "${selectedPreset}" speichern` ? `Änderungen in "${selectedPreset}" speichern`
: 'Aktuelle Regeln als neue Kombination speichern'} : 'Aktuelle Regeln als neue Kombination speichern'}
> >
<Icon name="save" size={14} /> <Icon name="save" size={14} />
+6 -6
View File
@@ -24,9 +24,9 @@ const TOOLS = {
['flip', 'Mirror', '_Mirror', 'Spiegeln'], ['flip', 'Mirror', '_Mirror', 'Spiegeln'],
['padding', 'Offset', '_Offset', 'Parallelversatz'], ['padding', 'Offset', '_Offset', 'Parallelversatz'],
['content_cut', 'Trim', '_Trim', 'Stutzen'], ['content_cut', 'Trim', '_Trim', 'Stutzen'],
['swipe_right_alt', 'Extend', '_Extend', 'Verlaengern'], ['swipe_right_alt', 'Extend', '_Extend', 'Verlängern'],
['link', 'Join', '_Join', 'Verbinden'], ['link', 'Join', '_Join', 'Verbinden'],
['scatter_plot', 'Explode', '_Explode', 'Aufloesen'], ['scatter_plot', 'Explode', '_Explode', 'Auflösen'],
['rounded_corner', 'Fillet', '_Fillet', 'Verrunden (Ecke abrunden)'], ['rounded_corner', 'Fillet', '_Fillet', 'Verrunden (Ecke abrunden)'],
['apps', 'Array', '_ArrayPolar','Polar-Array'], ['apps', 'Array', '_ArrayPolar','Polar-Array'],
], ],
@@ -41,11 +41,11 @@ const TOOLS = {
['unfold_more', 'Loft', '_Loft', 'Loft (Kurven verbinden)'], ['unfold_more', 'Loft', '_Loft', 'Loft (Kurven verbinden)'],
], ],
'Auswahl': [ 'Auswahl': [
['add_link', 'Chain', '_SelChain', 'Tangentiale Kurvenkette waehlen'], ['add_link', 'Chain', '_SelChain', 'Tangentiale Kurvenkette wählen'],
['filter_alt', 'Dup', '_SelDup', 'Doppelte Objekte waehlen'], ['filter_alt', 'Dup', '_SelDup', 'Doppelte Objekte wählen'],
['loop', 'Closed', '_SelClosedCrv', 'Geschlossene Kurven waehlen'], ['loop', 'Closed', '_SelClosedCrv', 'Geschlossene Kurven wählen'],
['compare_arrows', 'Invert', '_Invert', 'Auswahl invertieren'], ['compare_arrows', 'Invert', '_Invert', 'Auswahl invertieren'],
['select_all', 'All', '_SelAll', 'Alle auswaehlen'], ['select_all', 'All', '_SelAll', 'Alle auswählen'],
['deselect', 'None', '_SelNone', 'Auswahl aufheben'], ['deselect', 'None', '_SelNone', 'Auswahl aufheben'],
], ],
} }
+174 -2
View File
@@ -36,13 +36,29 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
color: null, color: null,
scale: 1.0, scale: 1.0,
rotation: 0, rotation: 0,
lw: null, // Stiftstaerke der Hatch in mm; null = wie Stift der Ebene lw: null,
...(ebene.fill || {}), ...(ebene.fill || {}),
}, },
// Section Style: was Rhino bei Clipping-Plane-Schnitten anzeigt.
// Spiegelt das native Section-Style-Dialog (Options Layer Section Style).
section: {
hatchPattern: 'None', // None / Solid / <name>
hatchColor: null, // null = ByObject (Layer-Farbe)
hatchRotation: 0,
hatchScale: 1.0,
background: 'viewport', // viewport / object
boundaryShow: true,
boundaryLinetype: 'byLayer',
boundaryWidthScale: 1.0,
boundaryColor: null, // null = ByObject
sectionOpenObjects: true,
...(ebene.section || {}),
},
}) })
const set = (patch) => setDraft({ ...draft, ...patch }) const set = (patch) => setDraft({ ...draft, ...patch })
const setFill = (patch) => setDraft({ ...draft, fill: { ...draft.fill, ...patch } }) const setFill = (patch) => setDraft({ ...draft, fill: { ...draft.fill, ...patch } })
const setSection = (patch) => setDraft({ ...draft, section: { ...draft.section, ...patch } })
const fill = draft.fill const fill = draft.fill
const isFilled = fill.pattern !== 'None' const isFilled = fill.pattern !== 'None'
@@ -50,6 +66,12 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
const fillFromLayer = fill.source === 'layer' const fillFromLayer = fill.source === 'layer'
const previewColor = (fillFromLayer || !fill.color) ? draft.color : fill.color const previewColor = (fillFromLayer || !fill.color) ? draft.color : fill.color
const sec = draft.section
const secHatched = sec.hatchPattern !== 'None'
const secHatchPat = secHatched && sec.hatchPattern !== 'Solid'
const secHatchColor = sec.hatchColor || draft.color
const secBoundColor = sec.boundaryColor || draft.color
// Pattern-Optionen: None + Solid + Patterns // Pattern-Optionen: None + Solid + Patterns
const patternOptions = [ const patternOptions = [
'None', 'Solid', 'None', 'Solid',
@@ -208,7 +230,7 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
<Field <Field
label="STIFTSTÄRKE (mm)" label="STIFTSTÄRKE (mm)"
hint="Leer = wie Stift der Ebene. Eigener Wert ueberschreibt die Strichstaerke der Hatch-Linien." hint="Leer = wie Stift der Ebene. Eigener Wert überschreibt die Strichstärke der Hatch-Linien."
> >
<input <input
type="number" step="0.01" min="0" type="number" step="0.01" min="0"
@@ -233,6 +255,156 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
</Field> </Field>
</> </>
)} )}
{/* === SECTION STYLE (Clipping-Plane-Schnitt) === */}
<SectionLabel>Section Style (Clipping-Schnitt)</SectionLabel>
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4, display: 'block', marginBottom: 4 }}>
Wird gezeigt wenn ein Objekt auf dieser Ebene von einer Clipping-Plane geschnitten wird.
</span>
<Field label="HATCH PATTERN">
<select
value={sec.hatchPattern}
onChange={(ev) => setSection({ hatchPattern: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
{patternOptions.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
</Field>
{secHatched && (
<>
<Field label="HATCH FARBE" hint="Leer = Stift der Ebene (By Object)">
<input
type="color"
value={secHatchColor}
onChange={(ev) => setSection({ hatchColor: ev.target.value })}
style={{
width: 32, height: 22, padding: 0,
border: '1px solid var(--border)', borderRadius: 'var(--r)',
cursor: 'pointer', background: 'transparent',
}}
/>
<span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-muted)', flex: 1 }}>
{sec.hatchColor || 'By Object'}
</span>
{sec.hatchColor && (
<button
className="btn-text"
style={{ fontSize: 10, padding: '2px 6px' }}
onClick={() => setSection({ hatchColor: null })}
>×</button>
)}
</Field>
{secHatchPat && (
<>
<Field label="HATCH SKALIERUNG">
<input
type="number" step="0.05" min="0.001"
value={sec.hatchScale}
onChange={(ev) => setSection({ hatchScale: parseFloat(ev.target.value) || 1.0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
<Field label="HATCH DREHUNG (°)">
<input
type="number" step="5"
value={sec.hatchRotation}
onChange={(ev) => setSection({ hatchRotation: parseFloat(ev.target.value) || 0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
</>
)}
<Field label="HINTERGRUND">
<select
value={sec.background}
onChange={(ev) => setSection({ background: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value="viewport">Viewport (transparent)</option>
<option value="object">By Object (Layer-Farbe)</option>
</select>
</Field>
</>
)}
{/* --- Boundary --- */}
<div style={{ marginTop: 8, padding: '4px 0',
borderTop: '1px dashed var(--border-light)' }}>
<span style={{ fontSize: 9, color: 'var(--text-muted)',
letterSpacing: 0.4, textTransform: 'uppercase' }}>
Boundary (Schnittkante)
</span>
</div>
<Field label="ZEIGE BOUNDARY">
<input
type="checkbox"
checked={sec.boundaryShow}
onChange={(ev) => setSection({ boundaryShow: ev.target.checked })}
style={{ marginRight: 6 }}
/>
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
{sec.boundaryShow ? 'Schnittkante wird gezeichnet' : 'Schnittkante unsichtbar'}
</span>
</Field>
{sec.boundaryShow && (
<>
<Field label="BOUNDARY FARBE" hint="Leer = Stift der Ebene (By Object)">
<input
type="color"
value={secBoundColor}
onChange={(ev) => setSection({ boundaryColor: ev.target.value })}
style={{
width: 32, height: 22, padding: 0,
border: '1px solid var(--border)', borderRadius: 'var(--r)',
cursor: 'pointer', background: 'transparent',
}}
/>
<span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-muted)', flex: 1 }}>
{sec.boundaryColor || 'By Object'}
</span>
{sec.boundaryColor && (
<button
className="btn-text"
style={{ fontSize: 10, padding: '2px 6px' }}
onClick={() => setSection({ boundaryColor: null })}
>×</button>
)}
</Field>
<Field
label="BOUNDARY WIDTH SCALE"
hint="Multiplikator auf die Ebenen-Stiftstärke. 1 = wie Ebene, 3 = 3× dicker."
>
<input
type="number" step="0.5" min="0.1" max="20"
value={sec.boundaryWidthScale}
onChange={(ev) => setSection({ boundaryWidthScale: parseFloat(ev.target.value) || 1.0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
</>
)}
<Field label="OFFENE OBJEKTE">
<input
type="checkbox"
checked={sec.sectionOpenObjects}
onChange={(ev) => setSection({ sectionOpenObjects: ev.target.checked })}
style={{ marginRight: 6 }}
/>
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
Auch nicht-geschlossene Geometrie schneiden
</span>
</Field>
</div> </div>
{/* Footer */} {/* Footer */}
+11 -1
View File
@@ -142,7 +142,17 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose }) {
}}> }}>
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<button className="btn-text" onClick={onClose}>Abbrechen</button> <button className="btn-text" onClick={onClose}>Abbrechen</button>
<button className="btn-contained" onClick={() => onSave(draft)}>Übernehmen</button> <button className="btn-contained" onClick={() => {
// Numerische Felder NIEMALS als undefined/null rausgehen lassen
// sonst crasht der Plugin spaeter beim float()-Cast. Defaults
// entsprechen den Werten die das UI auch ohne User-Input zeigt.
const out = { ...draft }
if (out.isGeschoss) {
if (out.hoehe == null) out.hoehe = 3.0
if (out.schnitthoehe == null) out.schnitthoehe = 1.0
}
onSave(out)
}}>Übernehmen</button>
</div> </div>
</div> </div>
</div> </div>
+12
View File
@@ -172,6 +172,13 @@ export function runCommand(cmd) { send('RUN_COMMAND', { cmd }) }
export function sendKeys(text, enter) { send('SEND_KEYS', { text, enter: enter !== false }) } export function sendKeys(text, enter) { send('SEND_KEYS', { text, enter: enter !== false }) }
export function cancelCommand() { send('CANCEL_COMMAND', {}) } export function cancelCommand() { send('CANCEL_COMMAND', {}) }
export function toggleRhinoCmdLine() { send('TOGGLE_RHINO_CMD_LINE', {}) } export function toggleRhinoCmdLine() { send('TOGGLE_RHINO_CMD_LINE', {}) }
export function getDossierSettings() { send('GET_SETTINGS', {}) }
export function openDossierSettings() { send('OPEN_SETTINGS', {}) }
export function applyWindowLayout(name) { send('APPLY_LAYOUT', { name }) }
export function saveLayoutPref(name, autoApply) {
send('SAVE_LAYOUT_PREF',
{ defaultLayout: name || '', autoApplyLayout: !!autoApply })
}
// --- Overrides-Panel --- // --- Overrides-Panel ---
export function setOverridesEnabled(on) { send('SET_ENABLED', { enabled: !!on }) } export function setOverridesEnabled(on) { send('SET_ENABLED', { enabled: !!on }) }
@@ -234,7 +241,12 @@ export function createDecke(p) { send('CREATE_DECKE', p || {}) }
export function createDach(p) { send('CREATE_DACH', p || {}) } export function createDach(p) { send('CREATE_DACH', p || {}) }
export function createFenster(p) { send('CREATE_FENSTER', p || {}) } export function createFenster(p) { send('CREATE_FENSTER', p || {}) }
export function createTuer(p) { send('CREATE_TUER', p || {}) } export function createTuer(p) { send('CREATE_TUER', p || {}) }
export function createAussparung(p) { send('CREATE_AUSSPARUNG', p || {}) }
export function createTreppe(p) { send('CREATE_TREPPE', p || {}) } export function createTreppe(p) { send('CREATE_TREPPE', p || {}) }
export function createStuetze(p) { send('CREATE_STUETZE', p || {}) }
export function createTraeger(p) { send('CREATE_TRAEGER', p || {}) }
export function createRaum(p) { send('CREATE_RAUM', p || {}) }
export function exportRaeume() { send('EXPORT_RAEUME', {}) }
export function updateElement(id, patch) { send('UPDATE_ELEMENT', { id, ...(patch || {}) }) } export function updateElement(id, patch) { send('UPDATE_ELEMENT', { id, ...(patch || {}) }) }
export function deleteElement(id) { send('DELETE_ELEMENT', { id }) } export function deleteElement(id) { send('DELETE_ELEMENT', { id }) }
// Backwards-Compat-Aliases // Backwards-Compat-Aliases