commit 9dc191be4fff4b55ea25894c942ec9299c932f4b Author: karim Date: Sat May 16 04:27:41 2026 +0200 Initial commit — Dossier Rhino 8 Plugin OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac): - Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde), Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel), Treppen (gerade · L · Wendel mit Schrittmass-Validierung) - Live-Previews mit Step-Lines + Soll-Range-Clamping - Bidirektionale Selection-Sync zwischen Source-Linie und Volume - Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz - Layouts mit PDF-Export - Ausschnitte / Massstab / Override-Regeln - Petrol-Gruen Theme (Rapport-konform) Co-Authored-By: Claude Opus 4.7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..574b77f --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Node / Vite +node_modules +dist +dist-ssr +*.local + +# Tauri (launcher/) +launcher/src-tauri/target/ +launcher/node_modules/ +launcher/dist/ +launcher/.tauri/ +*.key +*.key.pub + +# Python / Rhino +__pycache__/ +*.pyc +*.pyo + +# Claude Code +.claude/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# OS +Thumbs.db +ehthumbs.db +Desktop.ini diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..be1b7ed --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,160 @@ +# RhinoPanel — Projektdokumentation für Claude + +## Was ist das? + +Ein React-Plugin für Rhino 8 (Mac) das als schwebendes Fenster läuft und Architektur-Workflows aus Vectorworks/ArchiCAD nachbildet. Die React-UI wird in Rhinos Eto.Forms WebView über `LoadHtml` (inline) eingebettet. + +## Kommunikation React ↔ Python + +**React → Python:** `document.title = "RHINOMSG::{json}"` (queue-basiert, 80ms delay) +**Python → React:** `webview.ExecuteScript("window.onRhinoMessage({...})")` + +Nachrichten-Typen: +- `APPLY` — Ebenen auf Rhino anwenden, GH triggern +- `LAYER_VISIBILITY` — Layer sofort ein/ausblenden +- `LAYER_LOCK` — Layer sperren/entsperren +- `SET_ACTIVE` — Aktiven Rhino-Layer setzen +- `STATE_SYNC` — Python → React, beim Panel-Start + +## Datenmodell + +```json +[ + {"id": "eg", "name": "EG", "type": "grundriss", "hoehe": 3.50, "schnitthoehe": 1.00, "okff": 0.00}, + {"id": "1og", "name": "1OG", "type": "grundriss", "hoehe": 3.00, "schnitthoehe": 1.00, "okff": 3.50}, + {"id": "saa", "name": "Schnitt A-A", "type": "schnitt"}, + {"id": "nor", "name": "Nordansicht", "type": "ansicht"} +] +``` + +Gespeichert in `doc.Strings["rhinopanel_ebenen"]` (JSON). OKFF wird nur für `type: "grundriss"` berechnet (kumulativ). Schnitt/Ansicht haben keine Höhenparameter. + +## Rhino Layer-Hierarchie + +``` +10_GRUNDRISSE + └── EG + ├── 01_WAND (schwarz, lw 0.50) + ├── 02_TUER_FENSTER (blau, lw 0.25) + ├── 03_MOEBEL (grau, lw 0.13) + ├── 04_TEXT (hellgrau, lw 0.13) + ├── 05_TREPPEN (gold, lw 0.35) + └── 06_3D_VOLUMEN (lila, lw 0.25) + └── 1OG (gleiche Sublayer) +20_SCHNITTE + └── Schnitt A-A + ├── 21_PROFIL (rot, lw 0.70) + ├── 22_WAND (orange, lw 0.25) + └── 23_TEXT (hellgrau, lw 0.13) +30_ANSICHTEN + └── Nordansicht + ├── 31_FASSADE (türkis, lw 0.35) + └── 32_TEXT (hellgrau, lw 0.13) +00_RASTER, 01_VERMESSUNG, 40_SITUATION, 90_REFERENZEN, 99_KONSTRUKTION +``` + +## Dateistruktur + +``` +rhino-panel/ +├── src/ +│ ├── App.jsx # Hauptkomponente, State-Management +│ ├── main.jsx # Entry point, window.onerror handler +│ ├── index.css # Dark theme, CSS-Variablen (Swiss minimal) +│ ├── lib/ +│ │ └── rhinoBridge.js # Kommunikation React↔Python +│ └── components/ +│ ├── EbenenManager.jsx # Zeichnungsebenen-Liste (GR/SC/AN Badges) +│ ├── EbenenDialog.jsx # Dialog zum Bearbeiten der Ebenen +│ ├── LayerPanel.jsx # Layer-Sichtbarkeit/Sperre togglen +│ ├── BottomBar.jsx # "Auf Rhino anwenden" Button +│ └── Section.jsx # Ausklappbarer Abschnitt +├── rhino/ +│ ├── rhinopanel.py # Panel starten, Bridge, LoadHtml-Inline +│ ├── layer_builder.py # Rhino-Layer erstellen/aktualisieren +│ └── INSTALL.md # Setup-Anleitung inkl. GH +├── dist/ # Gebaute App (npm run build) +└── vite.config.js # base: './' wichtig für file:// URLs +``` + +**Alte Dateien (nicht mehr aktiv, können gelöscht werden):** +- `src/components/GeschossManager.jsx` — ersetzt durch EbenenManager +- `src/components/GeschossDialog.jsx` — ersetzt durch EbenenDialog + +## Kritische technische Details + +### Warum LoadHtml statt file:// URL +Rhinos WKWebView blockiert ` + + diff --git a/launcher/.gitignore b/launcher/.gitignore new file mode 100644 index 0000000..3bdd52e --- /dev/null +++ b/launcher/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.DS_Store diff --git a/launcher/README.md b/launcher/README.md new file mode 100644 index 0000000..00eec3a --- /dev/null +++ b/launcher/README.md @@ -0,0 +1,90 @@ +# Dossier Launcher + +Standalone macOS-App, die als Projekt-Hub für Dossier-Projekte in Rhino 8 dient. +Wählt eine `.3dm` aus, konfiguriert pro Projekt welche Module aktiv sind, startet +Rhino mit der Datei. Das Python-Plugin in `rhino/` liest beim Start die +`dossier.project.json` (neben der `.3dm`) und lädt nur die aktivierten Module. + +## Setup (einmalig) + +### 1. Dependencies installieren + +```bash +cd launcher +npm install +``` + +Beim ersten `npm run tauri dev` zieht Cargo zusätzlich die Rust-Dependencies +(dauert ein paar Minuten). + +### 2. Rhino Auto-Run einrichten + +Damit die Module bei jedem Rhino-Start automatisch laden: + +1. Rhino 8 starten +2. `Rhinoceros 8` → `Preferences` → `General` → **Startup commands** +3. Folgende Zeile eintragen: + ``` + _-RunPythonScript "/Users/karim/STUDIO/rhino-panel/rhino/startup.py" + ``` +4. OK → Rhino neu starten + +Ab jetzt lädt `startup.py` bei jedem Rhino-Start: +- mit `dossier.project.json` neben der `.3dm` → nur konfigurierte Module +- ohne Config → alle Module (Backwards-Compat zum bisherigen Verhalten) + +## Entwicklung + +```bash +npm run tauri dev +``` + +Startet Vite (Port 5174) und öffnet die Tauri-Window. Hot-Reload für React, +Rust-Änderungen brauchen einen Rebuild. + +## Build (.app + .dmg) + +```bash +npm run tauri build +``` + +Output: `src-tauri/target/release/bundle/macos/Dossier.app` und +`src-tauri/target/release/bundle/dmg/Dossier_0.1.0_*.dmg` + +**Vor dem ersten Build**: Icons hinterlegen. Aktuell ist `bundle.icon` in +`src-tauri/tauri.conf.json` leer. Mit +```bash +npm run tauri icon path/zur/icon.png +``` +generiert Tauri das vollständige Icon-Set (1024×1024 PNG als Quelle reicht). + +## Architektur + +``` +launcher/ +├── modules.json # Modul-Manifest (statisch, in Binary einkompiliert) +├── src/ # React-Frontend +│ ├── App.jsx # Project Hub + Modul-Dialog +│ └── styles.css +└── src-tauri/ # Rust-Backend + ├── src/lib.rs # Tauri-Commands + └── tauri.conf.json +``` + +**Datenfluss:** +1. Launcher liest `~/Library/Application Support/Dossier/recent.json` +2. User klickt „Öffnen" → Launcher schreibt `dossier.project.json` neben die + `.3dm` und ruft `open -a "Rhinoceros 8" file.3dm` +3. Rhino startet, `startup.py` läuft, liest die Config, lädt nur die aktivierten + Module +4. Jedes Modul registriert sein eigenes Panel via `panel_base.register_and_open` + +## Module-Manifest erweitern + +Wenn ein neues Modul dazukommt, **drei Stellen** synchron halten: + +1. `launcher/modules.json` — Eintrag mit `id`, `name`, `description`, `pythonModule`, `dependsOn` +2. `rhino/startup.py` — `_MODULE_TO_PY` Map ergänzen +3. `rhino/.py` — die Python-Implementierung + +Launcher rebuilden (`npm run tauri build`), neue `.app` ersetzt die alte. diff --git a/launcher/index.html b/launcher/index.html new file mode 100644 index 0000000..408dd28 --- /dev/null +++ b/launcher/index.html @@ -0,0 +1,12 @@ + + + + + + Dossier + + +
+ + + diff --git a/launcher/modules.json b/launcher/modules.json new file mode 100644 index 0000000..5853f4b --- /dev/null +++ b/launcher/modules.json @@ -0,0 +1,65 @@ +[ + { + "id": "ebenen", + "name": "Ebenen-Manager", + "description": "Zeichnungsebenen (Geschosse, Schnitte, Ansichten) verwalten.", + "pythonModule": "rhinopanel", + "dependsOn": [] + }, + { + "id": "oberleiste", + "name": "Oberleiste", + "description": "Massstab, Display-Mode, Snaps, Overrides — die Top-Toolbar.", + "pythonModule": "oberleiste", + "dependsOn": [] + }, + { + "id": "ausschnitte", + "name": "Ausschnitte", + "description": "Viewport-Ausschnitte mit Kamera, Massstab und Layer-Sichtbarkeit speichern.", + "pythonModule": "ausschnitte", + "dependsOn": ["oberleiste"] + }, + { + "id": "gestaltung", + "name": "Gestaltung", + "description": "Wand-Typen, Materialien, Texturen.", + "pythonModule": "gestaltung", + "dependsOn": ["ebenen"] + }, + { + "id": "werkzeuge", + "name": "Werkzeuge", + "description": "Hilfs-Werkzeuge fuer wiederkehrende Aufgaben.", + "pythonModule": "werkzeuge", + "dependsOn": [] + }, + { + "id": "overrides", + "name": "Overrides", + "description": "Layer-Style Overrides (Linientypen, Farben, Druck-Gewichte).", + "pythonModule": "overrides_panel", + "dependsOn": ["oberleiste"] + }, + { + "id": "dimensionen", + "name": "Dimensionen", + "description": "Object Info Palette: Position, Abmessungen, Drehung der Selektion bearbeiten (wie in Vectorworks).", + "pythonModule": "dimensionen", + "dependsOn": [] + }, + { + "id": "layouts", + "name": "Layouts", + "description": "Layouts erstellen und Details mit Ausschnitten bestuecken.", + "pythonModule": "layouts", + "dependsOn": ["ausschnitte"] + }, + { + "id": "elemente", + "name": "Elemente", + "description": "Smart-Elemente: Waende als Achse (editierbar) + Volumen (auto-generiert), verknuepft mit Geschossen.", + "pythonModule": "elemente", + "dependsOn": ["ebenen"] + } +] diff --git a/launcher/package-lock.json b/launcher/package-lock.json new file mode 100644 index 0000000..e723c72 --- /dev/null +++ b/launcher/package-lock.json @@ -0,0 +1,1706 @@ +{ + "name": "dossier-launcher", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dossier-launcher", + "version": "0.1.0", + "dependencies": { + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.0.0", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "@vitejs/plugin-react": "^6.0.1", + "esbuild": "^0.28.0", + "vite": "^8.0.12" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.1.tgz", + "integrity": "sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.1", + "@tauri-apps/cli-darwin-x64": "2.11.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.1", + "@tauri-apps/cli-linux-arm64-musl": "2.11.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.1", + "@tauri-apps/cli-linux-x64-gnu": "2.11.1", + "@tauri-apps/cli-linux-x64-musl": "2.11.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.1", + "@tauri-apps/cli-win32-x64-msvc": "2.11.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz", + "integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.1.tgz", + "integrity": "sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.1.tgz", + "integrity": "sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.1.tgz", + "integrity": "sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.1.tgz", + "integrity": "sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.1.tgz", + "integrity": "sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.1.tgz", + "integrity": "sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.1.tgz", + "integrity": "sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.1.tgz", + "integrity": "sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.1.tgz", + "integrity": "sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.1.tgz", + "integrity": "sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", + "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/rolldown": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/launcher/package.json b/launcher/package.json new file mode 100644 index 0000000..f5f354b --- /dev/null +++ b/launcher/package.json @@ -0,0 +1,24 @@ +{ + "name": "dossier-launcher", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.0.0", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "@vitejs/plugin-react": "^6.0.1", + "esbuild": "^0.28.0", + "vite": "^8.0.12" + } +} diff --git a/launcher/src-tauri/.gitignore b/launcher/src-tauri/.gitignore new file mode 100644 index 0000000..4cacddf --- /dev/null +++ b/launcher/src-tauri/.gitignore @@ -0,0 +1,2 @@ +/target +/gen diff --git a/launcher/src-tauri/Cargo.lock b/launcher/src-tauri/Cargo.lock new file mode 100644 index 0000000..320df8b --- /dev/null +++ b/launcher/src-tauri/Cargo.lock @@ -0,0 +1,4829 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dossier-launcher" +version = "0.1.0" +dependencies = [ + "chrono", + "directories", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93bd86d231f0a8138f11a02a584769fe4b703dc36ae133d783228dbc4801405" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a318b234cc2dea65f575467bafcfb76286bce228ebc3778e337d61d03213007" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd11644962add2549a60b7e7c6800f17d7020156e02f516021d8103e80cc528" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed9d3742a37a355d2e47c9af924e9fbc112abb76f9835d35d4780e318419502" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eefb2c18e8a605c23edb48fc56bb77381199e1a1e7f6ff0c9b970afe7b3cb8ee" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fef478ba1d2ac21c2d528740b24d0cb315e1e8b1111aae53fafac34804371fc" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d57200389a2f82b4b0a40ae29ca19b6978116e8f4d4e974c3234ce40c0ffbdec" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/launcher/src-tauri/Cargo.toml b/launcher/src-tauri/Cargo.toml new file mode 100644 index 0000000..b67a743 --- /dev/null +++ b/launcher/src-tauri/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "dossier-launcher" +version = "0.1.0" +description = "Dossier — Projekt-Launcher fuer Rhino" +authors = ["Karim Gabriele Varano"] +edition = "2021" + +[lib] +name = "dossier_launcher_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-dialog = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +directories = "5" +chrono = { version = "0.4", features = ["serde"] } + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/launcher/src-tauri/build.rs b/launcher/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/launcher/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/launcher/src-tauri/capabilities/default.json b/launcher/src-tauri/capabilities/default.json new file mode 100644 index 0000000..ccabc32 --- /dev/null +++ b/launcher/src-tauri/capabilities/default.json @@ -0,0 +1,10 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability fuer das Hauptfenster", + "windows": ["main"], + "permissions": [ + "core:default", + "dialog:default" + ] +} diff --git a/launcher/src-tauri/icons/icon.png b/launcher/src-tauri/icons/icon.png new file mode 100644 index 0000000..42ed240 Binary files /dev/null and b/launcher/src-tauri/icons/icon.png differ diff --git a/launcher/src-tauri/src/lib.rs b/launcher/src-tauri/src/lib.rs new file mode 100644 index 0000000..ee88bab --- /dev/null +++ b/launcher/src-tauri/src/lib.rs @@ -0,0 +1,174 @@ +// Verhindert ein extra Konsolen-Fenster auf Windows im Release-Build. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +// Statisches Modul-Manifest — in die Binary einkompiliert, sodass die App +// keine externe Datei zur Laufzeit braucht. Wer Module aendert: modules.json +// im launcher-Root bearbeiten, dann neu bauen. +const MODULES_JSON: &str = include_str!("../../modules.json"); + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct Project { + name: String, + path: String, + modules: Vec, + #[serde(rename = "lastOpened")] + last_opened: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct ProjectConfig { + name: String, + modules: Vec, + #[serde(rename = "dossierVersion")] + dossier_version: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +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")] + rhino_app: String, +} + +impl Default for Settings { + fn default() -> Self { + Self { + rhino_app: "Rhinoceros 8".into(), + } + } +} + +fn dossier_dir() -> PathBuf { + // ~/Library/Application Support/Dossier auf macOS, + // entsprechend Plattform-Pendants sonst. + let dir = directories::ProjectDirs::from("ch", "gabrielevarano", "Dossier") + .map(|p| p.data_dir().to_path_buf()) + .unwrap_or_else(|| PathBuf::from(".")); + fs::create_dir_all(&dir).ok(); + dir +} + +fn recent_path() -> PathBuf { + dossier_dir().join("recent.json") +} + +fn settings_path() -> PathBuf { + dossier_dir().join("settings.json") +} + +fn load_settings() -> Settings { + let p = settings_path(); + if !p.exists() { + return Settings::default(); + } + fs::read_to_string(&p) + .ok() + .and_then(|raw| serde_json::from_str(&raw).ok()) + .unwrap_or_default() +} + +#[tauri::command] +fn list_recent() -> Vec { + let p = recent_path(); + if !p.exists() { + return vec![]; + } + let raw = fs::read_to_string(&p).unwrap_or_default(); + serde_json::from_str(&raw).unwrap_or_default() +} + +#[tauri::command] +fn save_recent(projects: Vec) -> Result<(), String> { + let p = recent_path(); + let raw = serde_json::to_string_pretty(&projects).map_err(|e| e.to_string())?; + fs::write(&p, raw).map_err(|e| format!("recent.json schreiben: {e}")) +} + +#[tauri::command] +fn write_project_config( + path3dm: String, + name: String, + modules: Vec, +) -> Result<(), String> { + let p = Path::new(&path3dm); + let dir = p + .parent() + .ok_or_else(|| format!("Kein gueltiger Ordner aus Pfad: {path3dm}"))?; + let config_path = dir.join("dossier.project.json"); + let config = ProjectConfig { + name, + modules, + dossier_version: env!("CARGO_PKG_VERSION").to_string(), + }; + let raw = serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?; + fs::write(&config_path, raw).map_err(|e| format!("project-config schreiben: {e}")) +} + +#[tauri::command] +fn read_project_config(path3dm: String) -> Result, String> { + let p = Path::new(&path3dm); + let dir = p.parent().ok_or_else(|| "Pfad ungueltig".to_string())?; + let config_path = dir.join("dossier.project.json"); + if !config_path.exists() { + return Ok(None); + } + let raw = fs::read_to_string(&config_path).map_err(|e| e.to_string())?; + let cfg: ProjectConfig = serde_json::from_str(&raw).map_err(|e| e.to_string())?; + Ok(Some(cfg)) +} + +#[tauri::command] +fn open_rhino(path3dm: String) -> Result<(), String> { + // macOS: `open -a `. `` kann ein App-Name (in /Applications + // gesucht) oder ein absoluter Pfad zur .app sein. Aus den Settings — Default + // "Rhinoceros 8", falls der User nichts angepasst hat. + let settings = load_settings(); + Command::new("open") + .args(["-a", &settings.rhino_app, &path3dm]) + .spawn() + .map_err(|e| format!( + "open-Befehl fehlgeschlagen ({}): {e}", settings.rhino_app + ))?; + Ok(()) +} + +#[tauri::command] +fn read_settings() -> Settings { + load_settings() +} + +#[tauri::command] +fn save_settings(settings: Settings) -> Result<(), String> { + let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?; + fs::write(settings_path(), raw).map_err(|e| format!("settings.json schreiben: {e}")) +} + +#[tauri::command] +fn read_modules_manifest() -> Result { + serde_json::from_str(MODULES_JSON).map_err(|e| e.to_string()) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .invoke_handler(tauri::generate_handler![ + list_recent, + save_recent, + write_project_config, + read_project_config, + open_rhino, + read_modules_manifest, + read_settings, + save_settings + ]) + .run(tauri::generate_context!()) + .expect("Fehler beim Starten der Tauri-App"); +} diff --git a/launcher/src-tauri/src/main.rs b/launcher/src-tauri/src/main.rs new file mode 100644 index 0000000..be2ffcf --- /dev/null +++ b/launcher/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +// Tauri 2 Konvention: main.rs ist nur Einstieg, Logik in lib.rs (fuer Mobile- +// Unterstuetzung und damit `tauri::generate_context!` korrekt aufgeloest wird). +fn main() { + dossier_launcher_lib::run() +} diff --git a/launcher/src-tauri/tauri.conf.json b/launcher/src-tauri/tauri.conf.json new file mode 100644 index 0000000..2537a7c --- /dev/null +++ b/launcher/src-tauri/tauri.conf.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Dossier", + "version": "0.1.0", + "identifier": "ch.gabrielevarano.dossier", + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "(cd .. && npm run build) && npm run build", + "devUrl": "http://127.0.0.1:5183", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Dossier", + "width": 920, + "height": 640, + "minWidth": 720, + "minHeight": 480, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": ["app", "dmg"], + "icon": ["icons/icon.png"], + "copyright": "© 2026 Karim Gabriele Varano", + "category": "DeveloperTool", + "shortDescription": "Dossier Launcher", + "longDescription": "Projekt-Launcher fuer das Dossier-Plugin in Rhino 8.", + "resources": { + "../../dist": "dist", + "../../rhino": "rhino" + } + } +} diff --git a/launcher/src/App.jsx b/launcher/src/App.jsx new file mode 100644 index 0000000..60b213e --- /dev/null +++ b/launcher/src/App.jsx @@ -0,0 +1,325 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { invoke } from '@tauri-apps/api/core' +import { open as openDialog, save as saveDialog } from '@tauri-apps/plugin-dialog' + +// Transitive Dependency-Aufloesung: gibt Set aller Module zurueck, die +// aktiviert sein muessen, wenn `selected` aktiviert sind. +function resolveDeps(selected, allModules) { + const byId = Object.fromEntries(allModules.map(m => [m.id, m])) + const out = new Set(selected) + let changed = true + while (changed) { + changed = false + for (const id of [...out]) { + const m = byId[id] + if (!m) continue + for (const dep of (m.dependsOn || [])) { + if (!out.has(dep)) { out.add(dep); changed = true } + } + } + } + return out +} + +// Modul-IDs, die aktuell aktiv sind WEIL eine andere Auswahl sie braucht. +function lockedBy(selected, allModules) { + const byId = Object.fromEntries(allModules.map(m => [m.id, m])) + const locked = new Set() + for (const id of selected) { + const m = byId[id] + if (!m) continue + for (const dep of (m.dependsOn || [])) { + if (selected.has(dep) || selected.has(id)) locked.add(dep) + } + } + return locked +} + +function formatRelative(iso) { + if (!iso) return '' + try { + const d = new Date(iso) + const now = new Date() + const diff = (now - d) / 1000 + if (diff < 60) return 'vor wenigen Sek.' + if (diff < 3600) return `vor ${Math.floor(diff/60)} Min.` + if (diff < 86400) return `vor ${Math.floor(diff/3600)} h` + if (diff < 86400 * 7) return `vor ${Math.floor(diff/86400)} Tagen` + return d.toLocaleDateString('de-CH') + } catch { return '' } +} + +export default function App() { + const [recent, setRecent] = useState([]) + const [modules, setModules] = useState([]) + const [dialog, setDialog] = useState(null) // null | { mode: 'new'|'edit', project? } + const [settingsOpen, setSettingsOpen] = useState(false) + const [busy, setBusy] = useState(false) + + useEffect(() => { + invoke('list_recent').then(setRecent).catch(console.error) + invoke('read_modules_manifest').then(setModules).catch(console.error) + }, []) + + const refresh = () => invoke('list_recent').then(setRecent).catch(console.error) + + const openProject = async (proj) => { + if (busy) return + setBusy(true) + try { + await invoke('open_rhino', { path3dm: proj.path }) + const next = recent.map(p => + p.path === proj.path ? { ...p, lastOpened: new Date().toISOString() } : p + ) + await invoke('save_recent', { projects: next }) + setRecent(next) + } catch (ex) { + alert(`Rhino-Start fehlgeschlagen: ${ex}`) + } finally { + setBusy(false) + } + } + + const editProject = (proj) => { + setDialog({ mode: 'edit', project: proj }) + } + + const saveProject = async ({ name, path, modules: mods }) => { + if (!path) { alert('Bitte eine .3dm-Datei auswaehlen.'); return } + if (!name.trim()) { alert('Bitte einen Projekt-Namen angeben.'); return } + try { + await invoke('write_project_config', { path3dm: path, name: name.trim(), modules: mods }) + const others = recent.filter(p => p.path !== path) + const next = [{ name: name.trim(), path, modules: mods, + lastOpened: new Date().toISOString() }, ...others] + await invoke('save_recent', { projects: next }) + setRecent(next) + setDialog(null) + } catch (ex) { + alert(`Speichern fehlgeschlagen: ${ex}`) + } + } + + const removeProject = async (proj) => { + const next = recent.filter(p => p.path !== proj.path) + await invoke('save_recent', { projects: next }) + setRecent(next) + } + + return ( +
+
+ DOSSIER + 0.1.0 +
+ + +
+ +
+

Kuerzlich geoeffnet

+ {recent.length === 0 ? ( +
+ Noch keine Projekte. Klick „Neues Projekt" zum Starten. +
+ ) : ( +
+ {recent.map(p => ( +
openProject(p)}> +
+
{p.name}
+
{p.path}
+
+
+ {p.modules?.length || 0} Module · {formatRelative(p.lastOpened)} +
+ + + +
+ ))} +
+ )} +
+ + {dialog && ( + setDialog(null)} + onSave={saveProject} + /> + )} + {settingsOpen && ( + setSettingsOpen(false)} /> + )} +
+ ) +} + +function SettingsDialog({ onClose }) { + const [rhinoApp, setRhinoApp] = useState('') + const [loaded, setLoaded] = useState(false) + + useEffect(() => { + invoke('read_settings') + .then(s => { setRhinoApp(s.rhinoApp || 'Rhinoceros 8'); setLoaded(true) }) + .catch(() => { setRhinoApp('Rhinoceros 8'); setLoaded(true) }) + }, []) + + const pickRhino = async () => { + // .app-Bundles sind technisch Ordner, macOS behandelt sie aber im File-Picker + // als auswaehlbare Pakete, wenn der Extension-Filter "app" gesetzt ist. + // directory:true wuerde stattdessen reinnavigieren — falsch. + const sel = await openDialog({ + title: 'Rhino-App waehlen', + filters: [{ name: 'Anwendung', extensions: ['app'] }], + defaultPath: '/Applications', + }) + if (typeof sel === 'string') setRhinoApp(sel) + } + + const useDefault = () => setRhinoApp('Rhinoceros 8') + + const save = async () => { + try { + await invoke('save_settings', { settings: { rhinoApp: rhinoApp.trim() || 'Rhinoceros 8' } }) + onClose() + } catch (ex) { + alert(`Speichern fehlgeschlagen: ${ex}`) + } + } + + if (!loaded) return null + + return ( +
{ if (e.target === e.currentTarget) onClose() }}> +
+
Einstellungen
+
+
+ +
+ setRhinoApp(e.target.value)} + placeholder="Rhinoceros 8" /> + +
+
+ App-Name (in /Applications gesucht) oder absoluter Pfad zur .app. + Beispiele:
+ Rhinoceros 8 · /Applications/Rhino 8.app · /Applications/Rhino 8 WIP.app + +
+
+
+
+ + +
+
+
+ ) +} + +function ProjectDialog({ mode, project, modules, onCancel, onSave }) { + const [name, setName] = useState(project?.name || '') + const [path, setPath] = useState(project?.path || '') + const [picked, setPicked] = useState(new Set(project?.modules || ['ebenen', 'oberleiste'])) + + const effective = useMemo(() => resolveDeps(picked, modules), [picked, modules]) + const locked = useMemo(() => { + // Module, die aktiv sind, aber NICHT direkt gewaehlt — also nur als Dep + const out = new Set() + for (const id of effective) { + if (!picked.has(id)) out.add(id) + } + return out + }, [picked, effective, modules]) + + const toggle = (id) => { + if (locked.has(id)) return // nicht direkt abwaehlbar + setPicked(prev => { + const next = new Set(prev) + if (next.has(id)) next.delete(id); else next.add(id) + return next + }) + } + + const pickFile = async () => { + const sel = mode === 'new' + ? await openDialog({ + title: '3dm-Datei auswaehlen', + filters: [{ name: 'Rhino', extensions: ['3dm'] }], + }) + : await openDialog({ + title: '3dm-Datei auswaehlen', + filters: [{ name: 'Rhino', extensions: ['3dm'] }], + }) + if (typeof sel === 'string') setPath(sel) + } + + return ( +
{ if (e.target === e.currentTarget) onCancel() }}> +
+
{mode === 'new' ? 'Neues Projekt' : 'Projekt bearbeiten'}
+
+
+ + setName(e.target.value)} + placeholder="z.B. Wohnhaus Brunner" autoFocus /> +
+
+ +
+ + +
+
+
+ +
+ {modules.map(m => { + const isActive = effective.has(m.id) + const isLocked = locked.has(m.id) + return ( +
toggle(m.id)} + title={isLocked ? 'Wird von einem anderen Modul gebraucht' : ''} + > +
{isActive ? (isLocked ? '🔒' : '✓') : ''}
+
+
{m.name}
+
{m.description}
+ {(m.dependsOn || []).length > 0 && ( +
braucht: {m.dependsOn.join(', ')}
+ )} +
+
+ ) + })} +
+
+
+
+ + +
+
+
+ ) +} diff --git a/launcher/src/main.jsx b/launcher/src/main.jsx new file mode 100644 index 0000000..fe5a9e3 --- /dev/null +++ b/launcher/src/main.jsx @@ -0,0 +1,6 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.jsx' +import './styles.css' + +createRoot(document.getElementById('root')).render() diff --git a/launcher/src/styles.css b/launcher/src/styles.css new file mode 100644 index 0000000..0cc1156 --- /dev/null +++ b/launcher/src/styles.css @@ -0,0 +1,231 @@ +:root { + --bg-base: #1c1c1e; + --bg-panel: #2a2a2c; + --bg-elev: #34343a; + --border: #3a3a3e; + --text: #eaeaea; + --text-muted: #8a8a8e; + --accent: #5a9e5a; + --accent-hover: #6cb56c; + --danger: #c87050; + --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; + --mono: 'DM Mono', 'Menlo', monospace; +} + +* { box-sizing: border-box; } + +html, body, #root { + margin: 0; + padding: 0; + height: 100%; + background: var(--bg-base); + color: var(--text); + font-family: var(--font); + font-size: 13px; + -webkit-font-smoothing: antialiased; + user-select: none; +} + +button { + font-family: inherit; + font-size: inherit; + background: var(--bg-elev); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 12px; + cursor: pointer; + transition: background 0.1s ease; +} +button:hover:not(:disabled) { background: #404048; } +button:disabled { opacity: 0.4; cursor: not-allowed; } +button.primary { + background: var(--accent); + border-color: var(--accent); + color: white; +} +button.primary:hover:not(:disabled) { background: var(--accent-hover); } + +input[type="text"] { + font-family: inherit; + font-size: inherit; + background: var(--bg-base); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 10px; + outline: none; +} +input[type="text"]:focus { border-color: var(--accent); } + +.app { + display: flex; + flex-direction: column; + height: 100vh; +} +.topbar { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 20px; + border-bottom: 1px solid var(--border); + background: var(--bg-panel); +} +.topbar .brand { + font-family: var(--mono); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.12em; + color: var(--text-muted); +} +.topbar .version { + font-family: var(--mono); + font-size: 10px; + color: var(--text-muted); + opacity: 0.6; +} +.topbar .spacer { flex: 1; } + +.main { flex: 1; overflow-y: auto; padding: 20px; } + +.section-title { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-muted); + margin: 0 0 12px 0; +} + +.project-list { + display: flex; + flex-direction: column; + gap: 8px; +} +.project-card { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: 8px; + cursor: pointer; + transition: border-color 0.1s ease; +} +.project-card:hover { border-color: #555; } +.project-card .name { + font-weight: 500; + margin-bottom: 2px; +} +.project-card .path { + font-family: var(--mono); + font-size: 11px; + color: var(--text-muted); +} +.project-card .meta { + font-size: 11px; + color: var(--text-muted); +} + +.empty { + padding: 40px; + text-align: center; + color: var(--text-muted); + font-size: 12px; +} + +.module-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} +.module-row { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; +} +.module-row.locked { opacity: 0.7; cursor: default; } +.module-row .check { + flex-shrink: 0; + width: 16px; + height: 16px; + border-radius: 4px; + border: 1.5px solid var(--border); + background: var(--bg-base); + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + color: white; +} +.module-row.active .check { background: var(--accent); border-color: var(--accent); } +.module-row.locked .check { background: #555; border-color: #555; } +.module-row .info { flex: 1; min-width: 0; } +.module-row .info .name { font-weight: 500; margin-bottom: 2px; } +.module-row .info .desc { font-size: 11px; color: var(--text-muted); line-height: 1.4; } +.module-row .info .dep { + font-family: var(--mono); + font-size: 10px; + color: var(--text-muted); + margin-top: 4px; +} + +.dialog-bg { + position: fixed; inset: 0; + background: rgba(0,0,0,0.6); + display: flex; align-items: center; justify-content: center; + z-index: 10; +} +.dialog { + width: 600px; max-width: 90vw; + max-height: 90vh; + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: 10px; + display: flex; + flex-direction: column; + overflow: hidden; +} +.dialog header { + padding: 14px 18px; + border-bottom: 1px solid var(--border); + font-weight: 500; +} +.dialog .body { + padding: 18px; + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 14px; +} +.dialog .row { + display: flex; + flex-direction: column; + gap: 6px; +} +.dialog .row label { + font-size: 11px; + color: var(--text-muted); + letter-spacing: 0.05em; + text-transform: uppercase; +} +.dialog footer { + padding: 12px 18px; + border-top: 1px solid var(--border); + display: flex; + justify-content: flex-end; + gap: 8px; +} +.file-picker { + display: flex; + gap: 8px; + align-items: center; +} +.file-picker input { flex: 1; font-family: var(--mono); font-size: 11px; } diff --git a/launcher/vite.config.js b/launcher/vite.config.js new file mode 100644 index 0000000..55337c3 --- /dev/null +++ b/launcher/vite.config.js @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// Tauri-spezifisch: kein Browser-Open, fixer Port, ignoriert src-tauri fuer Hot-Reload. +export default defineConfig({ + plugins: [react()], + clearScreen: false, + server: { + // Eigener Port (nicht 5173/5174 — die nutzen andere Studio-Projekte wie + // Rapport). Host explizit IPv4, weil Tauri's devUrl auf 127.0.0.1 + // bindet — sonst kann Tauri auf [::1]: eines anderen Vite-Servers + // landen, wenn localhost zuerst zu IPv6 resolved. + port: 5183, + strictPort: true, + host: '127.0.0.1', + }, + envPrefix: ['VITE_', 'TAURI_'], + build: { + outDir: 'dist', + target: 'safari14', + // Vite 8: 'oxc' ist der eingebaute Rust-Minifier (default). 'esbuild' + // wuerde ein separates Paket erfordern — nicht noetig fuer uns. + minify: 'oxc', + sourcemap: false, + }, +}) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7a6269d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2458 @@ +{ + "name": "rhino-panel", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rhino-panel", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "vite": "^8.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.129.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", + "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", + "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", + "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", + "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", + "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", + "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", + "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", + "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", + "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", + "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", + "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", + "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", + "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", + "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", + "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", + "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/rolldown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", + "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.129.0", + "@rolldown/pluginutils": "1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0", + "@rolldown/binding-darwin-arm64": "1.0.0", + "@rolldown/binding-darwin-x64": "1.0.0", + "@rolldown/binding-freebsd-x64": "1.0.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", + "@rolldown/binding-linux-arm64-gnu": "1.0.0", + "@rolldown/binding-linux-arm64-musl": "1.0.0", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0", + "@rolldown/binding-linux-s390x-gnu": "1.0.0", + "@rolldown/binding-linux-x64-gnu": "1.0.0", + "@rolldown/binding-linux-x64-musl": "1.0.0", + "@rolldown/binding-openharmony-arm64": "1.0.0", + "@rolldown/binding-wasm32-wasi": "1.0.0", + "@rolldown/binding-win32-arm64-msvc": "1.0.0", + "@rolldown/binding-win32-x64-msvc": "1.0.0" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", + "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", + "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.0", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad47ab4 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "dossier", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "vite": "^8.0.12" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rhino/DOSSIERUI.rhw b/rhino/DOSSIERUI.rhw new file mode 100644 index 0000000..77644fb --- /dev/null +++ b/rhino/DOSSIERUI.rhw @@ -0,0 +1,666 @@ + + + + + + PAUSEUI + + + + + + + + Standard Toolbars + Standardní palety nástrojů + Standard-Werkzeugleisten + Barras de herramientas estándar + Barres d'outils Standard + Barre degli strumenti standard + 標準ツールバー + 표준 도구모음 + Standardowe paski narzędzi + Barras de Ferramentas Standard + 标准工具列 + 標準工具列 + Стандартные панели инструментов + + + + + + + + + + + + + + + + + + + + + + + + + + SubD Sidebar + Postranní panel SubD + SubD-Seitenleiste + SubD (lateral) + Volet SubD + Barra laterale SubD + SubDサイドバー + SubD 사이드바 + SubD - pasek boczny + Barra Lateral SubD + 细分边栏 + SubD 邊欄 + SubD - боковая + + + + + + + + + EBENEN + + + + + + + + + + + + + + + Container + + + + + + + + + + + + Display & Rendering + Zobrazení a renderování + Anzeige und Rendering + Visualización y renderizado + Affichage et rendu + Visualizzazione e rendering + 表示 & レンダリング + 표시와 렌더링 + Wyświetlanie i rendering + Visualização e Renderização + 显示 & 渲染 + 顯示 & 彩現 + Отображение и визуализация + + + + + + + + + + Solids Sidebar + Postranní panel Tělesa + Volumenkörper-Seitenleiste + Sólidos (lateral) + Volet Solides + Barra laterale Solidi + ソリッドサイドバー + 솔리드 사이드바 + Bryły - pasek boczny + Barra lateral de Sólidos + 实体边栏 + 實體邊欄 + Тела - боковая + + + + + + + + + OBERLEISTE + + + + + + + + + Curve drawing sidebar + Postranní panel Křivka + Kurvenzeichnung-Seitenleiste + Dibujo de curvas (lateral) + Volet Dessin de courbes + Barra laterale disegno curve + 曲線作成サイドバー + 커브 그리기 사이드바 + Rysowanie krzywych - pasek boczny + Barra lateral de desenhar curva + 曲线绘制边栏 + 繪製曲線邊欄 + Кривые - боковая + + + + + + + + + + + + Layers + + + + + + + + + Right Container + Pravý kontejner + Rechter Container + Contenedor derecho + Conteneur droit + Contenitore destro + 右コンテナ + 오른쪽 컨테이너 + Prawy zbiornik + Contentor Direito + 右侧容器 + 右側容器 + Правый контейнер + + + + + + + + + + + + + GESTALTUNG + + + + + + + + + OVERRIDES + + + + + + + + + Render Sidebar + Postranní panel Render + Render-Seitenleiste + Renderizado (lateral) + Volet Rendu + Barra laterale Rendering + レンダリングサイドバー + 렌더링 사이드바 + Rendering - pasek boczny + Barra lateral de Renderizar + 渲染边栏 + 彩現邊欄 + Визуализация - боковая + + + + + + + + + OBERLEISTE + + + + + + + + + WERKZEUGE + + + + + + + + + Named Views + Pojmenované pohledy + Benannte Ansichten + Vistas guardadas + Vues nommées + Viste con nome + 名前の付いたビュー + 명명된 뷰 + Nazwane widoki + Vistas Com Nome + 已命名视图 + 已命名視圖 + Именованные виды + + + + + + + + + + + + + + + + GESTALTUNG + + + + + + + + + Surface Sidebar + Postranní panel Plocha + Flächen-Seitenleiste + Superficies (lateral) + Volet Surface + Barra laterale Superfici + サーフェスサイドバー + 서피스 사이드바 + Powierzchnia - pasek boczny + Superfícies + 曲面边栏 + 曲面邊欄 + Поверхности - боковая + + + + + + + + + Command History + 명령 히스토리 + コマンドヒストリ + История команд + Befehlsverlauf + 指令历史 + Historial de comandos + Historique des commandes + 指令歷史 + Historia poleceń + Storico comandi + Historie příkazů + Histórico de Comandos + + + + + + + + + + Rectangle + + + + + + + + + Help + 도움말 + ヘルプ + Справка + Hilfe + 说明 + Ayuda + Aide + 說明 + Pomoc + Aiuti + Nápověda + Ajuda + + + + + + + + + WERKZEUGE + + + + + + + + + Main + Hlavní + Haupt + Principal + Principale + Principale + メイン + 메인 + Główne + Principal + 主要 + 主要 + Главная + + + + + + + + + OSnap + Uchop + Ofang + RefObj + Accrochages + Osnap + OSnap + 개체스냅 + UchwytOb + OSnap + 物件锁点 + 物件鎖點 + Привязка + + + + + + + + + + Layers + + + + + + + + + Mesh Sidebar + Postranní panel Síť + Polygonnetz-Seitenleiste + Malla (lateral) + Volet Maillage + Barra laterale Mesh + メッシュサイドバー + 메쉬 사이드바 + Siatka - pasek boczny + Barra lateral de Malha + 网格边栏 + 網格邊欄 + Сети - боковая + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Toolbar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <svg height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path clip-rule="evenodd" d="m0 0h36v36h-36z"/></clipPath><g clip-path="url(#a)" fill-opacity=".996078" transform="matrix(1.3333333 0 0 -1.3333333 0 48)"><path d="m14.297 18.846c0 1.657 1.345 3.002 3.002 3.002s3.002-1.345 3.002-3.002-1.345-3.002-3.002-3.002-3.002 1.345-3.002 3.002z" fill="#fff" fill-rule="evenodd"/><path d="m13.552 18.846c0 2.068 1.678 3.746 3.747 3.746 2.068 0 3.746-1.678 3.746-3.746 0-2.069-1.678-3.747-3.746-3.747-2.069 0-3.747 1.678-3.747 3.747zm6.004 0c0 1.245-1.012 2.257-2.257 2.257-1.246 0-2.258-1.012-2.258-2.257 0-1.246 1.012-2.258 2.258-2.258 1.245 0 2.257 1.012 2.257 2.258z"/></g></svg> + <svg height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g clip-path="url(#a)" fill-opacity=".996078" transform="matrix(1.3333333 0 0 -1.3333333 0 48)"> + <path d="m14.297 18.846c0 1.657 1.345 3.002 3.002 3.002s3.002-1.345 3.002-3.002-1.345-3.002-3.002-3.002-3.002 1.345-3.002 3.002z" fill="#fff" fill-rule="evenodd" stroke-width="3.5px" x_rma_id="1" stroke="#666" stroke-linejoin="round" /> + <path d="m13.552 18.846c0 2.068 1.678 3.746 3.747 3.746 2.068 0 3.746-1.678 3.746-3.746 0-2.069-1.678-3.747-3.746-3.747-2.069 0-3.747 1.678-3.747 3.747zm6.004 0c0 1.245-1.012 2.257-2.257 2.257-1.246 0-2.258-1.012-2.258-2.257 0-1.246 1.012-2.258 2.258-2.258 1.245 0 2.257 1.012 2.257 2.258z" fill="" stroke-width="3.5px" x_rma_id="2" stroke="#666" stroke-linejoin="round" /> + </g> + <clipPath id="a"> + <path clip-rule="evenodd" d="m0 0h36v36h-36z" fill="" stroke-width="" x_rma_id="0" /> + </clipPath> + <g clip-path="url(#a)" fill-opacity=".996078" transform="matrix(1.3333333 0 0 -1.3333333 0 48)"> + <path d="m14.297 18.846c0 1.657 1.345 3.002 3.002 3.002s3.002-1.345 3.002-3.002-1.345-3.002-3.002-3.002-3.002 1.345-3.002 3.002z" fill="#fff" fill-rule="evenodd" stroke-width="" x_rma_id="1" /> + <path d="m13.552 18.846c0 2.068 1.678 3.746 3.747 3.746 2.068 0 3.746-1.678 3.746-3.746 0-2.069-1.678-3.747-3.746-3.747-2.069 0-3.747 1.678-3.747 3.747zm6.004 0c0 1.245-1.012 2.257-2.257 2.257-1.246 0-2.258-1.012-2.258-2.257 0-1.246 1.012-2.258 2.258-2.258 1.245 0 2.257 1.012 2.257 2.258z" fill="" stroke-width="" x_rma_id="2" /> + </g> +</svg> + + + + + + Single point 01 + + Single point + Single point + Single point + Line + + + + + + + + \ No newline at end of file diff --git a/rhino/INSTALL.md b/rhino/INSTALL.md new file mode 100644 index 0000000..51975a2 --- /dev/null +++ b/rhino/INSTALL.md @@ -0,0 +1,98 @@ +# RhinoPanel — Installation Rhino 8 (Mac) + +## Voraussetzungen +- Rhino 8 +- Node.js (nur fuer Dev-Modus) + +--- + +## Dev-Modus + +```bash +cd rhino-panel +npm run dev # startet auf http://localhost:5173 +``` + +In Rhino: `_RunPythonScript` → `rhino/rhinopanel.py` + +## Prod-Modus + +```bash +npm run build # erstellt dist/ +``` + +In Rhino: `_RunPythonScript` → `rhinopanel.py` +Das Script erkennt `dist/index.html` automatisch. + +## Autostart + +Rhino Optionen → Scripting → Startup Scripts → `rhinopanel.py` hinzufuegen + +--- + +## Grasshopper-Anbindung + +### Wie es funktioniert + +Wenn "Anwenden" im Panel gedrueckt wird: +1. Rhino-Layer werden erstellt/aktualisiert +2. Ebenen-JSON wird in `doc.Strings["rhinopanel_ebenen"]` gespeichert +3. Grasshopper-Neuberechnung wird automatisch ausgeloest + +### GH Python-Component einrichten + +1. GH oeffnen → `Params > Util > Python Script` Komponente platzieren +2. Inhalt von `rhino/gh_ebenen.py` hineinkopieren +3. Outputs der Komponente (Rechtsklick → "Manage Outputs") anlegen: + +| Output | Typ | Inhalt | +|--------|-----|--------| +| `namen` | list | Grundriss-Namen (EG, 1OG, ...) | +| `okff` | list | Bodenniveau je Geschoss in m | +| `hoehen` | list | Geschosshoehe in m | +| `schnitthoehen` | list | Schnitthoehe ueber Boden in m | +| `grundriss_ebenen` | list | Planes auf Schnitthoehe (fuer Section) | +| `boden_ebenen` | list | Planes auf OKFF (fuer Extrusion) | +| `gebaeude_hoehe` | item | Gesamthoehe in m | +| `schnitte` | list | Namen der Schnitt-Ebenen | +| `ansichten` | list | Namen der Ansichts-Ebenen | + +### Typische GH-Verknuepfungen + +**Automatischer Grundrissschnitt:** +``` +gh_ebenen.grundriss_ebenen → Brep Split / Section +``` + +**Wandextrusion (2D → 3D):** +``` +Curves auf Layer 01_WAND + → Extrude (Vektor: {0,0,hoehe}) + → Cap Holes + → BooleanUnion +``` + +**Stockwerke positionieren:** +``` +gh_ebenen.boden_ebenen → Orient +``` + +### Alternativ: Get Document String (ohne Python) + +- `Params > Util > Get Document String` +- Key: `rhinopanel_ebenen` +- Output → `Deserialize JSON` oder `Evaluate Expression` + +--- + +## Dateistruktur + +``` +rhino-panel/ +├── src/ React-App (Quellcode) +├── dist/ Gebaute App (nach npm run build) +└── rhino/ + ├── rhinopanel.py Panel starten (in Rhino ausfuehren) + ├── layer_builder.py Layer-Erstellung + └── gh_ebenen.py GH Python-Component Code +``` diff --git a/rhino/ausschnitte.py b/rhino/ausschnitte.py new file mode 100644 index 0000000..636f5dc --- /dev/null +++ b/rhino/ausschnitte.py @@ -0,0 +1,708 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +ausschnitte.py +AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode, +Layer-Sichtbarkeit, DOSSIER-State. Anwendbar im Model-Space und auf Layout-Details. +""" +import os +import sys +import math +import json +import uuid +import Rhino +import System +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import panel_base + +PANEL_GUID_STR = "5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" +_STORE_KEY = "dossier_ausschnitte" +_FOLDERS_KEY = "dossier_ausschnitt_folders" +_PRESETS_KEY = "dossier_layer_presets" + + +def _orientation_from_camera(loc, tgt, parallel=True): + """Bestimmt Orientierung: + - perspective = perspektivische Projektion (kein orthogonaler Schnitt) + - horizontal = parallele Projektion mit Blick weitgehend nach unten/oben (Grundriss) + - vertical = parallele Projektion mit Blick weitgehend seitlich (Schnitt/Ansicht) + """ + if not parallel: + return "perspective" + try: + dx = tgt[0] - loc[0] + dy = tgt[1] - loc[1] + dz = tgt[2] - loc[2] + m = math.sqrt(dx * dx + dy * dy + dz * dz) + if m <= 0: return "vertical" + return "horizontal" if (abs(dz) / m) > 0.7 else "vertical" + except Exception: + return "vertical" + + +def _parse_scale(scale_str): + """Parse '1:50' / '1=50' / '50' → (page, model). Gibt None zurueck wenn nicht parsebar.""" + if not scale_str: + return None + s = scale_str.strip() + for sep in (":", "=", "/"): + if sep in s: + try: + a, b = s.split(sep, 1) + pa = float(a.strip()) + pb = float(b.strip()) + if pa > 0 and pb > 0: + return (pa, pb) + except Exception: + pass + break + # nur Zahl: 1:N angenommen + try: + n = float(s) + if n > 0: + return (1.0, n) + except Exception: + pass + return None + + +# --- Capture / Apply Helpers ------------------------------------------------ + +def _capture_camera(vp): + dm = vp.DisplayMode + dm_id = str(dm.Id) if dm else None + dm_nm = dm.LocalName if dm else None + loc = [vp.CameraLocation.X, vp.CameraLocation.Y, vp.CameraLocation.Z] + tgt = [vp.CameraTarget.X, vp.CameraTarget.Y, vp.CameraTarget.Z] + # Frustum-Breite mitsichern: bei Parallelprojektion bestimmt sie den Zoom. + # Ohne sie laesst sich der gespeicherte Ausschnitt nicht rekonstruieren. + frustum_w = None + frustum_h = None + try: + ok, l, r, b, t, n, f = vp.GetFrustum() + if ok: + frustum_w = float(r - l) + frustum_h = float(t - b) + except Exception: + pass + return { + "viewName": vp.Name, + "location": loc, + "target": tgt, + "up": [vp.CameraUp.X, vp.CameraUp.Y, vp.CameraUp.Z], + "lens": float(vp.Camera35mmLensLength) if vp.Camera35mmLensLength else 50.0, + "parallel": bool(vp.IsParallelProjection), + "displayMode": dm_id, + "displayModeName": dm_nm, + "orientation": _orientation_from_camera(loc, tgt, bool(vp.IsParallelProjection)), + "frustumWidth": frustum_w, + "frustumHeight": frustum_h, + } + + +def _apply_camera(vp, cam): + if not cam: return + try: + loc = Rhino.Geometry.Point3d(*cam["location"]) + tgt = Rhino.Geometry.Point3d(*cam["target"]) + up = Rhino.Geometry.Vector3d(*cam["up"]) if cam.get("up") else None + if cam.get("parallel"): + vp.ChangeToParallelProjection(True) + else: + vp.ChangeToPerspectiveProjection(True, float(cam.get("lens", 50))) + vp.SetCameraLocations(tgt, loc) + if up: + try: vp.CameraUp = up + except Exception: pass + dm_id = cam.get("displayMode") + if dm_id: + try: + mode = Rhino.Display.DisplayModeDescription.GetDisplayMode(System.Guid(dm_id)) + if mode is not None: vp.DisplayMode = mode + except Exception: + pass + # Zoom (Frustum-Breite) rekonstruieren — nur bei Parallelprojektion sinnvoll. + # Bei Perspective bestimmt die Lens-Length den Bildausschnitt, dort + # waere ein Magnify kontraproduktiv. + fw = cam.get("frustumWidth") + if fw and fw > 0 and cam.get("parallel"): + try: + ok, l, r, b, t, n, f = vp.GetFrustum() + if ok: + cur_w = float(r - l) + if cur_w > 0: + factor = cur_w / float(fw) + if 1e-9 < factor < 1e9: + try: + vp.Magnify(float(factor), False) + except Exception: + try: + vp.Magnify(float(factor)) + except Exception: + Rhino.RhinoApp.RunScript( + "_-Zoom _Factor {:.6f} _Enter".format(factor), False) + except Exception as ex: + print("[AUSSCHNITTE] Frustum-Apply:", ex) + except Exception as ex: + print("[AUSSCHNITTE] Camera-Apply:", ex) + + +def _capture_layers(doc): + out = [] + for layer in doc.Layers: + if layer.IsDeleted: continue + out.append({ + "id": str(layer.Id), + "visible": bool(layer.IsVisible), + "locked": bool(layer.IsLocked), + }) + return out + + +def _apply_layers_global(doc, layers): + by_id = {} + for layer in doc.Layers: + if not layer.IsDeleted: + by_id[str(layer.Id)] = layer + for ls in layers: + layer = by_id.get(ls.get("id")) + if layer is None: continue + if layer.IsVisible != ls.get("visible", True): + layer.IsVisible = ls.get("visible", True) + if layer.IsLocked != ls.get("locked", False): + layer.IsLocked = ls.get("locked", False) + + +def _apply_layers_per_viewport(doc, layers, vp_id): + """Setzt Sichtbarkeit pro Viewport (fuer Layout-Details).""" + by_id = {} + for layer in doc.Layers: + if not layer.IsDeleted: + by_id[str(layer.Id)] = layer + for ls in layers: + layer = by_id.get(ls.get("id")) + if layer is None: continue + try: + layer.SetPerViewportVisible(vp_id, ls.get("visible", True)) + except Exception: + pass + + +_DOSSIER_KEYS = ( + "dossier_zeichnungsebenen", + "dossier_ebenen", + "dossier_active_id", + "dossier_active_code", +) + + +def _capture_dossier_state(doc): + out = {} + for k in _DOSSIER_KEYS: + v = doc.Strings.GetValue(k) + if v is not None: + out[k] = v + return out + + +def _apply_dossier_state(doc, state): + for k, v in (state or {}).items(): + if v is not None: + doc.Strings.SetString(k, v) + + +def _find_selected_detail(doc): + """Sucht nach einem aktuell selektierten Detail-Viewport-Objekt.""" + for obj in doc.Objects.GetSelectedObjects(False, False): + if isinstance(obj, Rhino.DocObjects.DetailViewObject): + return obj + return None + + +def _load_snapshots(doc): + """Modul-interne Snapshot-Liste (cross-modul nutzbar).""" + raw = doc.Strings.GetValue(_STORE_KEY) + if not raw: return [] + try: + data = json.loads(raw) + return data if isinstance(data, list) else [] + except Exception: + return [] + + +def apply_snapshot_to_detail(doc, detail, snap_id): + """Wendet einen Ausschnitt auf ein konkretes Detail-Object an. Wird vom + Layouts-Modul benutzt, um Ausschnitt-Detail-Bindings zu synchronisieren. + Liefert True bei Erfolg.""" + snap = next((s for s in _load_snapshots(doc) if s.get("id") == snap_id), None) + if not snap: + print("[AUSSCHNITTE] apply_to_detail: snap nicht gefunden", snap_id) + return False + # Page-View ermitteln (fuer SetActiveDetail/SetPageAsActive) + page_view = None + try: + for view in doc.Views: + if isinstance(view, Rhino.Display.RhinoPageView): + try: + if any(d.Id == detail.Id for d in view.GetDetailViews()): + page_view = view + break + except Exception: + continue + except Exception as ex: + print("[AUSSCHNITTE] page-view-suche:", ex) + # Detail muss aktiv sein, damit Kamera-Aenderungen anschlagen + was_active = False + try: was_active = detail.IsActive + except Exception: pass + if page_view is not None and not was_active: + try: page_view.SetActiveDetail(detail.Id) + except Exception as ex: print("[AUSSCHNITTE] SetActiveDetail:", ex) + # Kamera + Layer + Name + vp = detail.Viewport + _apply_camera(vp, snap.get("camera")) + _apply_layers_per_viewport(doc, snap.get("layers", []), vp.Id) + try: + new_name = snap.get("name") + if new_name and vp.Name != new_name: + vp.Name = new_name + except Exception as ex: + print("[AUSSCHNITTE] Detail-Rename:", ex) + # Massstab + ratio = _parse_scale(snap.get("scale", "")) + if ratio is not None: + page_v, model_v = ratio + for label, setter in ( + ("DetailGeometry.SetScale", lambda: detail.DetailGeometry.SetScale(model_v, page_v)), + ("Detail.SetScale", lambda: detail.SetScale(model_v, page_v)), + ): + try: + setter() + break + except Exception: + continue + # Commit + Deaktivieren + try: detail.CommitViewportChanges() + except Exception: + try: detail.CommitChanges() + except Exception: pass + if page_view is not None and not was_active: + try: page_view.SetPageAsActive() + except Exception: pass + try: + (page_view or doc.Views).Redraw() + except Exception: + doc.Views.Redraw() + print("[AUSSCHNITTE] '{}' auf Detail {} angewendet".format(snap.get("name"), detail.Id)) + return True + + +# --- Bridge ----------------------------------------------------------------- + +class AusschnittBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "ausschnitte") + + def _on_ready(self): + self._send_list() + + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): p = {} + + if t == "READY": self._on_ready() + elif t == "LIST": self._send_list() + elif t == "SAVE": self._save(p.get("name", "Ausschnitt")) + elif t == "UPDATE": self._update(p.get("id")) + elif t == "RESTORE": self._restore(p.get("id")) + elif t == "APPLY_TO_DETAIL":self._apply_to_detail(p.get("id")) + elif t == "RENAME": self._rename(p.get("id"), p.get("name")) + elif t == "DELETE": self._delete(p.get("id")) + elif t == "SET_FOLDER": self._set_field(p.get("id"), "folder", p.get("folder") or "") + elif t == "SET_SCALE": self._set_field(p.get("id"), "scale", p.get("scale") or "") + elif t == "DUPLICATE": self._duplicate(p.get("id")) + elif t == "ADD_FOLDER": self._add_folder(p.get("name")) + elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name")) + elif t == "GET_LAYERS": self._send_layers(p.get("id")) + elif t == "UPDATE_LAYERS": self._update_layers(p.get("id"), p.get("layers") or []) + elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or []) + elif t == "DELETE_PRESET": self._delete_preset(p.get("name")) + + def _load(self, doc): + raw = doc.Strings.GetValue(_STORE_KEY) + if not raw: return [] + try: + data = json.loads(raw) + return data if isinstance(data, list) else [] + except Exception: + return [] + + def _store(self, doc, snaps): + doc.Strings.SetString(_STORE_KEY, json.dumps(snaps, ensure_ascii=False)) + + def _load_folders(self, doc): + raw = doc.Strings.GetValue(_FOLDERS_KEY) + if not raw: return [] + try: + data = json.loads(raw) + return data if isinstance(data, list) else [] + except Exception: + return [] + + def _store_folders(self, doc, folders): + doc.Strings.SetString(_FOLDERS_KEY, json.dumps(folders, ensure_ascii=False)) + + def _load_presets(self, doc): + raw = doc.Strings.GetValue(_PRESETS_KEY) + if not raw: return [] + try: + data = json.loads(raw) + return data if isinstance(data, list) else [] + except Exception: + return [] + + def _store_presets(self, doc, presets): + doc.Strings.SetString(_PRESETS_KEY, json.dumps(presets, ensure_ascii=False)) + + def _send_list(self): + doc = Rhino.RhinoDoc.ActiveDoc + snaps = self._load(doc) + explicit_folders = self._load_folders(doc) + # Aus Snapshots zusaetzliche Ordner ableiten (falls Snap auf nicht-existenten Ordner zeigt) + for s in snaps: + f = s.get("folder", "") + if f and f not in explicit_folders: + explicit_folders.append(f) + slim = [] + for s in snaps: + cam = s.get("camera", {}) or {} + orient = cam.get("orientation") + if not orient: + loc = cam.get("location") or [0, 0, 0] + tgt = cam.get("target") or [0, 0, 0] + orient = _orientation_from_camera(loc, tgt, bool(cam.get("parallel", True))) + slim.append({ + "id": s.get("id"), + "name": s.get("name"), + "folder": s.get("folder", ""), + "scale": s.get("scale", ""), + "orientation": orient, + "displayModeName": cam.get("displayModeName"), + "parallel": cam.get("parallel", False), + }) + preset_summary = [{"name": p.get("name"), "count": len(p.get("layers") or [])} + for p in self._load_presets(doc)] + self.send("LIST", { + "snapshots": slim, + "folders": explicit_folders, + "presets": preset_summary, + }) + + def _add_folder(self, name): + if not name: return + name = name.strip() + if not name: return + doc = Rhino.RhinoDoc.ActiveDoc + folders = self._load_folders(doc) + if name not in folders: + folders.append(name) + self._store_folders(doc, folders) + self._send_list() + + def _remove_folder(self, name): + if not name: return + doc = Rhino.RhinoDoc.ActiveDoc + # Aus Folder-Liste entfernen + folders = [f for f in self._load_folders(doc) if f != name] + self._store_folders(doc, folders) + # Snapshots aus diesem Ordner herausnehmen (auf root) + snaps = self._load(doc) + for s in snaps: + if s.get("folder") == name: + s["folder"] = "" + self._store(doc, snaps) + self._send_list() + + def _duplicate(self, snap_id): + if not snap_id: return + doc = Rhino.RhinoDoc.ActiveDoc + snaps = self._load(doc) + src = next((s for s in snaps if s.get("id") == snap_id), None) + if not src: return + # Tiefe Kopie via JSON + copy = json.loads(json.dumps(src, ensure_ascii=False)) + copy["id"] = "snap_" + uuid.uuid4().hex[:8] + copy["name"] = (src.get("name", "Ausschnitt") + " Kopie") + # Direkt nach Original einfuegen + idx = snaps.index(src) + snaps.insert(idx + 1, copy) + self._store(doc, snaps) + self._send_list() + print("[AUSSCHNITTE] '{}' dupliziert".format(src.get("name"))) + + def _set_field(self, snap_id, field, value): + if not snap_id: return + doc = Rhino.RhinoDoc.ActiveDoc + snaps = self._load(doc) + for s in snaps: + if s.get("id") == snap_id: + s[field] = value + break + self._store(doc, snaps) + self._send_list() + + def _capture(self, doc, name, existing_id=None, prior_scale=""): + view = doc.Views.ActiveView + if view is None: + print("[AUSSCHNITTE] Keine aktive View") + return None + vp = view.ActiveViewport + # Aktuelle Skala vom MASSSTAB-Modul holen — nur sinnvoll bei Parallel- + # projektion. In Perspective bleibt scale leer (Fallback: prior_scale). + scale_str = "" + try: + import massstab + # Bewusst der EINGESTELLTE Wert (User-Intent), nicht der live aus + # dem Viewport berechnete. Letzterer drifted bei Pan/Zoom. + ratio = massstab.get_applied_scale_ratio() + if ratio is not None and ratio > 0: + if ratio >= 10: + scale_str = "1:{:.0f}".format(ratio) + else: + scale_str = "1:{:.1f}".format(ratio) + except Exception as ex: + print("[AUSSCHNITTE] Live-Skala lesen:", ex) + # Fallback: wenn kein Massstab gepinnt war, die aus dem Frustum + # berechnete Live-Skala speichern. So bleibt das Massstab-Dropdown + # nach Restore konsistent (auch wenn der eigentliche Zoom-Restore + # bereits ueber frustumWidth in _apply_camera laeuft). + if not scale_str: + try: + import massstab + live = massstab.get_current_scale_ratio() + if live is not None and live > 0: + scale_str = "1:{:.0f}".format(live) if live >= 10 else "1:{:.1f}".format(live) + except Exception as ex: + print("[AUSSCHNITTE] Live-Skala (Fallback):", ex) + if not scale_str and prior_scale: + scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben + return { + "id": existing_id or "snap_" + uuid.uuid4().hex[:8], + "name": name, + "scale": scale_str, + "camera": _capture_camera(vp), + "layers": _capture_layers(doc), + "dossier": _capture_dossier_state(doc), + } + + def _save(self, name): + doc = Rhino.RhinoDoc.ActiveDoc + snap = self._capture(doc, name) + if snap is None: return + snaps = self._load(doc) + snaps.append(snap) + self._store(doc, snaps) + self._send_list() + print("[AUSSCHNITTE] '{}' gespeichert".format(name)) + + def _update(self, snap_id): + doc = Rhino.RhinoDoc.ActiveDoc + snaps = self._load(doc) + target = next((s for s in snaps if s.get("id") == snap_id), None) + if not target: return + updated = self._capture(doc, target.get("name", "Ausschnitt"), + existing_id=snap_id, + prior_scale=target.get("scale", "")) + if updated is None: return + for i, s in enumerate(snaps): + if s.get("id") == snap_id: + snaps[i] = updated + break + self._store(doc, snaps) + self._send_list() + print("[AUSSCHNITTE] '{}' aktualisiert".format(target.get("name"))) + + def _restore(self, snap_id): + doc = Rhino.RhinoDoc.ActiveDoc + snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None) + if not snap: return + view = doc.Views.ActiveView + if view is None: return + vp = view.ActiveViewport + _apply_camera(vp, snap.get("camera")) + _apply_layers_global(doc, snap.get("layers", [])) + _apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {}) + # Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py + # wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt + # _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten + # Ausschnitt und der neue zeigt "1:?". + try: + new_name = snap.get("name") + if new_name and vp.Name != new_name: + vp.Name = new_name + except Exception as ex: + print("[AUSSCHNITTE] Rename:", ex) + # Gespeicherten Massstab anwenden (z.B. "1:50") — falls vorhanden und + # Viewport parallel ist (in Perspective ignoriert massstab._apply_scale). + try: + scale_str = (snap.get("scale") or "").strip() + if scale_str: + ratio = _parse_scale(scale_str) + if ratio: + _, model_v = ratio # (page=1, model=N) -> N + import massstab + massstab._apply_scale(doc, vp, float(model_v)) + print("[AUSSCHNITTE] Massstab gesetzt auf 1:{} (applied={})".format( + model_v, massstab.get_applied_scale_ratio())) + # Andere Panels (Massstab, Oberleiste) sofort ueber den + # neuen appliedScale informieren — sonst zeigt das Dropdown + # noch den vorherigen Wert bis zum naechsten Idle-Tick mit + # Aenderung an der Live-Skala. + for key in ("massstab_bridge", "oberleiste_bridge"): + try: + b = sc.sticky.get(key) + print("[AUSSCHNITTE] force-send via {}: {}".format(key, "OK" if b is not None else "MISSING")) + if b is not None: + b._send_state(force=True) + except Exception as e: + print("[AUSSCHNITTE] force-send {} failed: {}".format(key, e)) + except Exception as ex: + print("[AUSSCHNITTE] Massstab-Restore:", ex) + view.Redraw() + print("[AUSSCHNITTE] '{}' wiederhergestellt".format(snap.get("name"))) + + def _apply_to_detail(self, snap_id): + doc = Rhino.RhinoDoc.ActiveDoc + # 1) Detail aus Selektion oder aktiver PageView ermitteln + detail = _find_selected_detail(doc) + if detail is None: + try: + av = doc.Views.ActiveView + if isinstance(av, Rhino.Display.RhinoPageView): + for d in av.GetDetailViews(): + if d.IsActive: + detail = d + break + except Exception as ex: + print("[AUSSCHNITTE] Active-Detail-Suche:", ex) + if detail is None: + print("[AUSSCHNITTE] Kein Detail ausgewaehlt — bitte:") + print(" 1) ins Layout wechseln") + print(" 2) Detail-Rahmen einmal anklicken (so dass er hervorgehoben ist)") + print(" 3) erneut 'Auf Detail anwenden' waehlen") + return + # 2) Delegieren an den oeffentlichen Helper + apply_snapshot_to_detail(doc, detail, snap_id) + + def _send_layers(self, snap_id): + if not snap_id: return + doc = Rhino.RhinoDoc.ActiveDoc + snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None) + if not snap: + print("[AUSSCHNITTE] Snap nicht gefunden:", snap_id) + return + snap_by_id = {} + for ls in (snap.get("layers") or []): + snap_by_id[ls.get("id")] = ls + + layers = [] + for layer in doc.Layers: + if layer.IsDeleted: continue + lid = str(layer.Id) + ls = snap_by_id.get(lid, {}) + layers.append({ + "id": lid, + "name": layer.Name, + "fullPath": layer.FullPath, + "color": "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B), + "visible": bool(ls.get("visible", layer.IsVisible)), + "locked": bool(ls.get("locked", layer.IsLocked)), + }) + layers.sort(key=lambda x: x["fullPath"]) + presets = self._load_presets(doc) + self.send("LAYERS_DATA", { + "id": snap_id, + "name": snap.get("name"), + "layers": layers, + "presets": presets, + }) + + def _update_layers(self, snap_id, layers): + if not snap_id: return + doc = Rhino.RhinoDoc.ActiveDoc + snaps = self._load(doc) + target = next((s for s in snaps if s.get("id") == snap_id), None) + if not target: return + new_list = [] + for ls in layers: + lid = ls.get("id") + if not lid: continue + new_list.append({ + "id": lid, + "visible": bool(ls.get("visible", True)), + "locked": bool(ls.get("locked", False)), + }) + target["layers"] = new_list + self._store(doc, snaps) + self._send_list() + print("[AUSSCHNITTE] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name"))) + + def _save_preset(self, name, layers): + if not name: return + name = name.strip() + if not name: return + doc = Rhino.RhinoDoc.ActiveDoc + presets = self._load_presets(doc) + clean = [] + for ls in layers: + lid = ls.get("id") + if not lid: continue + clean.append({ + "id": lid, + "visible": bool(ls.get("visible", True)), + "locked": bool(ls.get("locked", False)), + }) + existing = next((p for p in presets if p.get("name") == name), None) + if existing is not None: + existing["layers"] = clean + else: + presets.append({"name": name, "layers": clean}) + self._store_presets(doc, presets) + self._send_list() + print("[AUSSCHNITTE] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean))) + + def _delete_preset(self, name): + if not name: return + doc = Rhino.RhinoDoc.ActiveDoc + presets = [p for p in self._load_presets(doc) if p.get("name") != name] + self._store_presets(doc, presets) + self._send_list() + + def _rename(self, snap_id, name): + if not snap_id or not name: return + doc = Rhino.RhinoDoc.ActiveDoc + snaps = self._load(doc) + for s in snaps: + if s.get("id") == snap_id: + s["name"] = name + break + self._store(doc, snaps) + self._send_list() + + def _delete(self, snap_id): + if not snap_id: return + doc = Rhino.RhinoDoc.ActiveDoc + snaps = [s for s in self._load(doc) if s.get("id") != snap_id] + self._store(doc, snaps) + self._send_list() + + +panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge, + icon_spec=("A", "#c87050")) diff --git a/rhino/clean.py b/rhino/clean.py new file mode 100644 index 0000000..b7b1a22 --- /dev/null +++ b/rhino/clean.py @@ -0,0 +1,48 @@ +# ! python3 +""" +clean.py +Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste +startup.py-Lauf die Bridges komplett neu erzeugt. + +WICHTIG: Zusaetzlich muessen die drei Panels in Rhinos Layout +einmal geschlossen werden (Rechtsklick auf Tab -> Schliessen), +damit Rhino den alten Panel-Inhalt nicht weiter verwendet. +""" +import scriptcontext as sc + +KEYS = [ + # Alte Keys (rueckwaerts-kompatibel) + "rhinopanel_registered", "rhinopanel_form", "rhinopanel_guid", + # panel_base-System + "panel_registered_ebenen", "panel_guid_ebenen", + "panel_registered_gestaltung", "panel_guid_gestaltung", + "panel_registered_ausschnitte","panel_guid_ausschnitte", + # EBENEN + "ebenen_layer_listener", + "ebenen_bridge_ref", + "ebenen_processing_layer", + "ebenen_processing", + # GESTALTUNG + "gestaltung_selection_listener", + "gestaltung_bridge", +] + +removed = 0 +for k in KEYS: + if k in sc.sticky: + del sc.sticky[k] + removed += 1 + +# Sicherheitshalber auch alle anderen panel_*/ebenen_*/gestaltung_*-Keys raus +for k in list(sc.sticky.keys()): + if isinstance(k, str) and ( + k.startswith("panel_") or k.startswith("ebenen_") or + k.startswith("gestaltung_") or k.startswith("ausschnitt") + ): + del sc.sticky[k] + removed += 1 + +print("[clean] {} Sticky-Keys entfernt.".format(removed)) +print("[clean] Schliesse jetzt die EBENEN / GESTALTUNG / AUSSCHNITTE") +print("[clean] Panels im Rhino-Layout (Rechtsklick Tab -> Schliessen),") +print("[clean] dann startup.py neu ausfuehren.") diff --git a/rhino/clean_layers.py b/rhino/clean_layers.py new file mode 100644 index 0000000..2631e33 --- /dev/null +++ b/rhino/clean_layers.py @@ -0,0 +1,51 @@ +# ! python3 +""" +clean_layers.py +Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.) +die keine Objekte enthalten und nicht zum RhinoPanel gehoeren. +Ausfuehren via _RunPythonScript. +""" +import re +import Rhino +import scriptcontext as sc + +sc.sticky["rhinopanel_registered"] = False +sc.sticky["rhinopanel_form"] = None + +doc = Rhino.RhinoDoc.ActiveDoc + +PROTECTED = { + "10_grundrisse", "20_schnitte", "30_ansichten", + "00_raster", "01_vermessung", "40_situation", + "90_referenzen", "99_konstruktion", +} + +pattern = re.compile(r'^(default|layer\s*0*\d+)$', re.IGNORECASE) + +gone = [] +skip = [] + +for i in range(doc.Layers.Count - 1, -1, -1): + layer = doc.Layers[i] + root = layer.FullPath.split("::")[0].strip().lower() + if root in PROTECTED: + continue + if not pattern.match(layer.Name.strip()): + continue + try: + if doc.Layers.Delete(i, True): + gone.append(layer.Name) + else: + skip.append(layer.Name) + except Exception: + skip.append(layer.Name) + +doc.Views.Redraw() + +if gone: + print("[clean_layers] Geloescht: {}".format(", ".join(gone))) +else: + print("[clean_layers] Nichts geloescht (schon sauber?)") +if skip: + print("[clean_layers] Uebersprungen (Objekte drauf): {}".format(", ".join(skip))) +print("[clean_layers] Panel-Sticky zurueckgesetzt") diff --git a/rhino/dimensionen.py b/rhino/dimensionen.py new file mode 100644 index 0000000..9344f3f --- /dev/null +++ b/rhino/dimensionen.py @@ -0,0 +1,612 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +dimensionen.py +DIMENSIONEN-Panel: Object Info Palette nach Vectorworks-Vorbild. +Zeigt Position + Abmessungen der Selektion an und erlaubt direktes Eintippen +mit 9-Punkt-Referenz, World/CPlane-Modus und Shape-spezifischen Feldern +(Kreis, Linie, Rechteck). +""" +import os +import sys +import math +import Rhino +import Rhino.Geometry as rg +import System +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import panel_base + +PANEL_GUID_STR = "9e3c8c5d-6d4a-4f3e-b3c5-d4e5f6071a2c" + +# Idle-Polling fuer geometrische Aenderungen (Gumball-Move feuert keine +# SelectObjects-Events). Tick alle N Idle-Calls — N hoeher = weniger CPU. +_IDLE_GEOM_POLL = 8 + + +# --- Geometrie-Helpers ------------------------------------------------------ + +def _get_selected_objects(doc): + """Liste aller aktuell selektierten RhinoObjects.""" + if doc is None: return [] + try: + return list(doc.Objects.GetSelectedObjects(False, False)) + except Exception: + return [] + + +def _get_cplane(doc): + """Aktive Construction Plane oder Plane.WorldXY als Fallback.""" + try: + v = doc.Views.ActiveView + if v is not None: + cp = v.ActiveViewport.ConstructionPlane() + if cp is not None and cp.Plane.IsValid: + return cp.Plane + except Exception: + pass + return rg.Plane.WorldXY + + +def _active_plane(doc, mode): + """Plane fuer die aktuelle Koordinatenangabe — World oder CPlane.""" + if mode == "cplane": + return _get_cplane(doc) + return rg.Plane.WorldXY + + +def _bbox_in_plane(objs, plane): + """BBox aller selektierten Objekte im Koordinatensystem der gegebenen + Plane (achsen-aligned zur Plane). Liefert (BoundingBox, plane) oder None.""" + if not objs: + return None + # World -> Plane Transform anwenden -> BBox in plane-Koordinaten + xform = rg.Transform.PlaneToPlane(plane, rg.Plane.WorldXY) + bbox = rg.BoundingBox.Empty + for obj in objs: + try: + geom = obj.Geometry + if geom is None: continue + bb = geom.GetBoundingBox(xform) + if bb.IsValid: + bbox.Union(bb) + except Exception: + pass + return bbox if bbox.IsValid else None + + +def _ref_point_local(bbox, ref): + """Referenzpunkt in plane-lokalen Koordinaten anhand ref-Dict + {x: 'min'|'mid'|'max', y: ..., z: ...}.""" + def axis(amin, amax, code): + if code == "min": return amin + if code == "max": return amax + return (amin + amax) * 0.5 + return rg.Point3d( + axis(bbox.Min.X, bbox.Max.X, ref.get("x", "min")), + axis(bbox.Min.Y, bbox.Max.Y, ref.get("y", "min")), + axis(bbox.Min.Z, bbox.Max.Z, ref.get("z", "mid")), + ) + + +def _ref_point_world(bbox_local, ref, plane): + """Referenzpunkt in Welt-Koordinaten: lokal -> plane.PointAt.""" + p_local = _ref_point_local(bbox_local, ref) + return plane.PointAt(p_local.X, p_local.Y, p_local.Z) + + +def _round(v, digits=4): + try: + return round(float(v), digits) + except Exception: + return v + + +# --- Shape-Detection -------------------------------------------------------- + +def _detect_shape(objs): + """Erkennt spezifische Formen: Kreis, Linie, Rechteck (geschlossene + planare Polyline mit 4 perpendikularen Segmenten). Liefert dict oder None. + Nur bei genau einem selektierten Curve-Objekt.""" + if len(objs) != 1: + return None + obj = objs[0] + geom = obj.Geometry + if not isinstance(geom, rg.Curve): + return None + # Kreis? + try: + ok, circle = geom.TryGetCircle(0.001) + if ok and circle.IsValid: + return { + "type": "circle", + "radius": _round(circle.Radius, 4), + "center": [_round(circle.Center.X), _round(circle.Center.Y), _round(circle.Center.Z)], + } + except Exception: + pass + # Linie? + try: + if isinstance(geom, rg.LineCurve): + line = geom.Line + length = line.Length + angle_deg = math.degrees(math.atan2(line.Direction.Y, line.Direction.X)) + return { + "type": "line", + "length": _round(length, 4), + "angle": _round(angle_deg, 3), + "start": [_round(line.From.X), _round(line.From.Y), _round(line.From.Z)], + "end": [_round(line.To.X), _round(line.To.Y), _round(line.To.Z)], + } + except Exception: + pass + # Rechteck als geschlossene Polyline mit 4 perpendikularen Segmenten? + try: + ok, poly = geom.TryGetPolyline() + if ok and poly is not None and poly.Count == 5 and poly[0].DistanceTo(poly[-1]) < 1e-6: + pts = [poly[i] for i in range(4)] + v0 = pts[1] - pts[0] + v1 = pts[2] - pts[1] + v2 = pts[3] - pts[2] + v3 = pts[0] - pts[3] + def _dot(a, b): return a.X * b.X + a.Y * b.Y + a.Z * b.Z + # Adjacente Kanten perpendikular? + if (abs(_dot(v0, v1)) < 1e-4 and + abs(_dot(v1, v2)) < 1e-4 and + abs(_dot(v2, v3)) < 1e-4): + w = v0.Length + h = v1.Length + return { + "type": "rectangle", + "width": _round(w, 4), + "height": _round(h, 4), + } + except Exception: + pass + return None + + +# --- Transform-Operationen -------------------------------------------------- + +def _apply_xform(doc, objs, xform): + """Transform auf alle Objekte anwenden (in-place via ID).""" + if not xform.IsValid: return 0 + n = 0 + for obj in objs: + try: + if doc.Objects.Transform(obj.Id, xform, True): + n += 1 + except Exception as ex: + print("[DIMENSIONEN] Transform-Fehler:", ex) + return n + + +# --- Undo-Wrapper ----------------------------------------------------------- +# Ohne BeginUndoRecord/EndUndoRecord wird ein Multi-Objekt-Transform nicht +# zuverlaessig als ein einziger Undo-Schritt registriert — Ctrl+Z ueberspringt +# dann unsere Aenderung. Wir packen jede User-Aktion in einen benannten Record. + +class _UndoRecord(object): + def __init__(self, doc, label): + self.doc = doc + self.label = label + self.serial = 0 + def __enter__(self): + try: + self.serial = self.doc.BeginUndoRecord(self.label) + except Exception as ex: + print("[DIMENSIONEN] BeginUndoRecord:", ex) + self.serial = 0 + return self + def __exit__(self, exc_type, exc_val, exc_tb): + if self.serial: + try: self.doc.EndUndoRecord(self.serial) + except Exception as ex: + print("[DIMENSIONEN] EndUndoRecord:", ex) + return False # exceptions propagieren + + +def _translate_in_plane(doc, objs, plane, dx, dy, dz): + """Verschiebt um (dx, dy, dz) in plane-lokalen Achsen.""" + if dx == 0 and dy == 0 and dz == 0: return + delta_world = plane.XAxis * dx + plane.YAxis * dy + plane.ZAxis * dz + xform = rg.Transform.Translation(delta_world) + with _UndoRecord(doc, "Dossier: Position aendern"): + _apply_xform(doc, objs, xform) + doc.Views.Redraw() + + +def _scale_around_point(doc, objs, plane, ref_world, sx, sy, sz): + """Skalierung mit eigenen Faktoren pro Achse, zentriert am Referenzpunkt, + ausgerichtet an plane.""" + if sx == 1 and sy == 1 and sz == 1: return + if sx <= 0 or sy <= 0 or sz <= 0: + print("[DIMENSIONEN] Ungueltige Skalierungsfaktoren:", sx, sy, sz) + return + p = rg.Plane(plane) + p.Origin = ref_world + xform = rg.Transform.Scale(p, sx, sy, sz) + with _UndoRecord(doc, "Dossier: Abmessung aendern"): + _apply_xform(doc, objs, xform) + doc.Views.Redraw() + + +def _rotate_around_axis(doc, objs, ref_world, axis_dir, angle_deg): + """Rotation um axis_dir durch ref_world.""" + if angle_deg == 0: return + xform = rg.Transform.Rotation(math.radians(angle_deg), axis_dir, ref_world) + with _UndoRecord(doc, "Dossier: Drehen"): + _apply_xform(doc, objs, xform) + doc.Views.Redraw() + + +# --- Shape-Edit-Operationen ------------------------------------------------- + +def _set_circle_radius(doc, obj, new_radius, plane, ref_world): + """Skaliert ein Kreis-Curve so, dass es genau new_radius hat — Referenz + bleibt fix. Wird ueber globale Scale realisiert, damit das Objekt + konsistent mit dem Rest selektierter Objekte transformiert wird.""" + geom = obj.Geometry + ok, circle = geom.TryGetCircle(0.001) + if not ok or circle.Radius <= 0: + return False + factor = float(new_radius) / circle.Radius + if factor <= 0: return False + p = rg.Plane(plane) + p.Origin = ref_world + xform = rg.Transform.Scale(p, factor, factor, factor) + return bool(doc.Objects.Transform(obj.Id, xform, True)) + + +def _set_line_length(doc, obj, new_length, ref_world): + """Linie so verlaengern/verkuerzen, dass sie new_length hat. Skaliert + Linie entlang ihrer Richtung um den Referenzpunkt.""" + geom = obj.Geometry + if not isinstance(geom, rg.LineCurve): return False + line = geom.Line + cur = line.Length + if cur <= 0: return False + factor = float(new_length) / cur + if factor <= 0: return False + # Linie skaliert sich nur entlang ihrer Direction. Scale-1D ueber eine + # Plane mit der Linien-Direction als X-Achse waere ideal — vereinfacht: + # uniformer Scale, falls Linie achsen-parallel zur lokalen X-Plane + # ist das aequivalent zu Length-Scaling. + xaxis = rg.Vector3d(line.Direction) + xaxis.Unitize() + yaxis = rg.Vector3d.CrossProduct(rg.Vector3d.ZAxis, xaxis) + if yaxis.Length < 1e-6: + yaxis = rg.Vector3d.YAxis + yaxis.Unitize() + plane = rg.Plane(ref_world, xaxis, yaxis) + xform = rg.Transform.Scale(plane, factor, 1.0, 1.0) + return bool(doc.Objects.Transform(obj.Id, xform, True)) + + +def _set_rectangle_dims(doc, obj, new_w, new_h, plane, ref_world): + """Skaliert Rechteck-Curve auf (new_w, new_h). Annahme: width = Laenge + erster Seite (v0), height = zweiter Seite (v1) in der Polyline- + Reihenfolge — entspricht der Reihenfolge aus _detect_shape.""" + geom = obj.Geometry + if not isinstance(geom, rg.Curve): return False + ok, poly = geom.TryGetPolyline() + if not ok or poly is None or poly.Count != 5: return False + pts = [poly[i] for i in range(4)] + v0 = pts[1] - pts[0] + v1 = pts[2] - pts[1] + w_cur = v0.Length + h_cur = v1.Length + if w_cur <= 0 or h_cur <= 0: return False + sw = float(new_w) / w_cur + sh = float(new_h) / h_cur + if sw <= 0 or sh <= 0: return False + # Achsen des Rechtecks als Plane fuer den Scale + xaxis = rg.Vector3d(v0); xaxis.Unitize() + yaxis = rg.Vector3d(v1); yaxis.Unitize() + rect_plane = rg.Plane(ref_world, xaxis, yaxis) + xform = rg.Transform.Scale(rect_plane, sw, sh, 1.0) + return bool(doc.Objects.Transform(obj.Id, xform, True)) + + +# --- Bridge ----------------------------------------------------------------- + +class DimensionenBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "dimensionen") + self._ref = {"x": "min", "y": "min", "z": "mid"} + self._coord_sys = "world" # "world" | "cplane" + self._last_sig = None + self._last_ids = () + self._idle_cnt = 0 + + def _on_ready(self): + self._send_state(force=True) + + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): p = {} + + if t == "READY": self._on_ready() + elif t == "REQUEST_STATE": self._send_state(force=True) + elif t == "SET_REF_POINT": self._set_ref_point(p) + elif t == "SET_COORD_SYSTEM": self._set_coord_system(p) + elif t == "SET_POSITION": self._set_position(p) + elif t == "SET_DIMENSION": self._set_dimension(p) + elif t == "SET_ROTATION_Z": self._set_rotation_z(p) + elif t == "SET_CIRCLE_RADIUS":self._set_circle_radius(p) + elif t == "SET_LINE_LENGTH": self._set_line_length(p) + elif t == "SET_RECTANGLE": self._set_rectangle(p) + + # --- State-Snapshot ----------------------------------------------------- + + def _compute_state(self): + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: + return {"selection": {"count": 0, "type": "none", "shape": None}, + "refPoint": self._ref, "coordSystem": self._coord_sys} + objs = _get_selected_objects(doc) + plane = _active_plane(doc, self._coord_sys) + bbox_local = _bbox_in_plane(objs, plane) + + # Typ der Selektion + typ = "none" + if len(objs) == 0: + typ = "none" + elif len(objs) == 1: + g = objs[0].Geometry + if isinstance(g, rg.Curve): typ = "curve" + elif isinstance(g, rg.Brep): typ = "brep" + elif isinstance(g, rg.Mesh): typ = "mesh" + elif isinstance(g, rg.Extrusion): typ = "extrusion" + elif isinstance(g, rg.InstanceReferenceGeometry): typ = "block" + elif isinstance(g, rg.Point): typ = "point" + elif isinstance(g, rg.TextEntity): typ = "text" + else: typ = "other" + else: + typ = "mixed" + + out = { + "selection": {"count": len(objs), "type": typ}, + "refPoint": self._ref, + "coordSystem": self._coord_sys, + "planeName": "CPlane" if self._coord_sys == "cplane" else "Welt", + } + shape = _detect_shape(objs) + out["shape"] = shape + + if bbox_local is None: + out["position"] = None + out["dimensions"] = None + return out + + # Position des Referenzpunkts (in plane-lokalen Koordinaten — das ist + # das, was der User typischerweise sehen will: World ist Plane=WorldXY, + # CPlane ist Plane=ActiveCPlane). + ref_local = _ref_point_local(bbox_local, self._ref) + out["position"] = { + "x": _round(ref_local.X, 4), + "y": _round(ref_local.Y, 4), + "z": _round(ref_local.Z, 4), + } + # Abmessungen (Plane-aligned BBox-Spannweite) + out["dimensions"] = { + "width": _round(bbox_local.Max.X - bbox_local.Min.X, 4), + "depth": _round(bbox_local.Max.Y - bbox_local.Min.Y, 4), + "height": _round(bbox_local.Max.Z - bbox_local.Min.Z, 4), + } + return out + + def _send_state(self, force=False): + state = self._compute_state() + # Signature fuer Diff — komplette JSON-Repr ist ok bei kleinen Dicts + sig = ( + state.get("selection", {}).get("count"), + state.get("selection", {}).get("type"), + (state.get("shape") or {}).get("type"), + state.get("coordSystem"), + tuple(sorted((state.get("refPoint") or {}).items())), + tuple(sorted((state.get("position") or {}).items())), + tuple(sorted((state.get("dimensions") or {}).items())), + tuple(sorted((state.get("shape") or {}).items())) if state.get("shape") else None, + ) + if not force and sig == self._last_sig: + return + self._last_sig = sig + self.send("STATE", state) + + def tick_idle(self): + # 1) Schnelle Selektions-Erkennung: ID-Liste vergleichen + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + try: + objs = _get_selected_objects(doc) + ids = tuple(sorted(str(o.Id) for o in objs)) + if ids != self._last_ids: + self._last_ids = ids + self._send_state(force=True) + self._idle_cnt = 0 + return + except Exception: + pass + # 2) Geometrie kann sich aendern ohne Selektions-Change (Gumball-Move). + # Niedriger-frequenter Geom-Poll. + self._idle_cnt += 1 + if self._idle_cnt >= _IDLE_GEOM_POLL: + self._idle_cnt = 0 + self._send_state(force=False) + + # --- Handler ------------------------------------------------------------ + + def _set_ref_point(self, p): + ref = self._ref + for k in ("x", "y", "z"): + v = p.get(k) + if v in ("min", "mid", "max"): + ref[k] = v + self._send_state(force=True) + + def _set_coord_system(self, p): + mode = p.get("mode") + if mode in ("world", "cplane"): + self._coord_sys = mode + self._send_state(force=True) + + def _set_position(self, p): + """Verschiebt Selektion so, dass der Referenzpunkt auf den neuen + Wert kommt. Nur die angegebene Achse wird geaendert.""" + doc = Rhino.RhinoDoc.ActiveDoc + objs = _get_selected_objects(doc) + if not objs: return + axis = p.get("axis") + try: value = float(p.get("value")) + except Exception: return + if axis not in ("x", "y", "z"): return + plane = _active_plane(doc, self._coord_sys) + bbox_local = _bbox_in_plane(objs, plane) + if bbox_local is None: return + ref_local = _ref_point_local(bbox_local, self._ref) + dx = dy = dz = 0.0 + if axis == "x": dx = value - ref_local.X + if axis == "y": dy = value - ref_local.Y + if axis == "z": dz = value - ref_local.Z + _translate_in_plane(doc, objs, plane, dx, dy, dz) + self._send_state(force=True) + + def _set_dimension(self, p): + """Skaliert Selektion in der angegebenen Achse, sodass der angegebene + Wert die neue Plane-aligned BBox-Spannweite ist. Referenzpunkt bleibt + fix.""" + doc = Rhino.RhinoDoc.ActiveDoc + objs = _get_selected_objects(doc) + if not objs: return + axis = p.get("axis") # 'width' | 'depth' | 'height' + try: value = float(p.get("value")) + except Exception: return + if axis not in ("width", "depth", "height"): return + if value <= 0: return + plane = _active_plane(doc, self._coord_sys) + bbox_local = _bbox_in_plane(objs, plane) + if bbox_local is None: return + ref_world = _ref_point_world(bbox_local, self._ref, plane) + cur_w = bbox_local.Max.X - bbox_local.Min.X + cur_d = bbox_local.Max.Y - bbox_local.Min.Y + cur_h = bbox_local.Max.Z - bbox_local.Min.Z + sx = sy = sz = 1.0 + if axis == "width" and cur_w > 0: sx = value / cur_w + if axis == "depth" and cur_d > 0: sy = value / cur_d + if axis == "height" and cur_h > 0: sz = value / cur_h + _scale_around_point(doc, objs, plane, ref_world, sx, sy, sz) + self._send_state(force=True) + + def _set_rotation_z(self, p): + """Rotiert Selektion um die Z-Achse der aktiven Plane durch den + Referenzpunkt um angle Grad.""" + doc = Rhino.RhinoDoc.ActiveDoc + objs = _get_selected_objects(doc) + if not objs: return + try: angle = float(p.get("angle")) + except Exception: return + plane = _active_plane(doc, self._coord_sys) + bbox_local = _bbox_in_plane(objs, plane) + if bbox_local is None: return + ref_world = _ref_point_world(bbox_local, self._ref, plane) + _rotate_around_axis(doc, objs, ref_world, plane.ZAxis, angle) + self._send_state(force=True) + + def _set_circle_radius(self, p): + doc = Rhino.RhinoDoc.ActiveDoc + objs = _get_selected_objects(doc) + if len(objs) != 1: return + try: radius = float(p.get("value")) + except Exception: return + if radius <= 0: return + plane = _active_plane(doc, self._coord_sys) + bbox_local = _bbox_in_plane(objs, plane) + if bbox_local is None: return + ref_world = _ref_point_world(bbox_local, self._ref, plane) + with _UndoRecord(doc, "Dossier: Radius aendern"): + _set_circle_radius(doc, objs[0], radius, plane, ref_world) + doc.Views.Redraw() + self._send_state(force=True) + + def _set_line_length(self, p): + doc = Rhino.RhinoDoc.ActiveDoc + objs = _get_selected_objects(doc) + if len(objs) != 1: return + try: length = float(p.get("value")) + except Exception: return + if length <= 0: return + plane = _active_plane(doc, self._coord_sys) + bbox_local = _bbox_in_plane(objs, plane) + if bbox_local is None: return + ref_world = _ref_point_world(bbox_local, self._ref, plane) + with _UndoRecord(doc, "Dossier: Linienlaenge aendern"): + _set_line_length(doc, objs[0], length, ref_world) + doc.Views.Redraw() + self._send_state(force=True) + + def _set_rectangle(self, p): + doc = Rhino.RhinoDoc.ActiveDoc + objs = _get_selected_objects(doc) + if len(objs) != 1: return + try: + w = float(p.get("width")) + h = float(p.get("height")) + except Exception: + return + if w <= 0 or h <= 0: return + plane = _active_plane(doc, self._coord_sys) + bbox_local = _bbox_in_plane(objs, plane) + if bbox_local is None: return + ref_world = _ref_point_world(bbox_local, self._ref, plane) + with _UndoRecord(doc, "Dossier: Rechteck-Abmessung"): + _set_rectangle_dims(doc, objs[0], w, h, plane, ref_world) + doc.Views.Redraw() + self._send_state(force=True) + + +# --- Listener-Installation -------------------------------------------------- + +def _install_listeners(bridge): + flag = "dimensionen_listeners" + sc.sticky["dimensionen_bridge"] = bridge + if sc.sticky.get(flag): + return + + def on_idle(s, e): + b = sc.sticky.get("dimensionen_bridge") + if b is not None: + try: b.tick_idle() + except Exception as ex: print("[DIMENSIONEN] idle:", ex) + + def on_select(s, e): + b = sc.sticky.get("dimensionen_bridge") + if b is not None: + try: b._send_state(force=True) + except Exception: pass + + Rhino.RhinoApp.Idle += on_idle + try: + Rhino.RhinoDoc.SelectObjects += on_select + Rhino.RhinoDoc.DeselectObjects += on_select + Rhino.RhinoDoc.DeselectAllObjects += on_select + except Exception as ex: + print("[DIMENSIONEN] select-events:", ex) + sc.sticky[flag] = True + print("[DIMENSIONEN] Listener aktiv (Idle + SelectObjects)") + + +def _bridge_factory(): + b = DimensionenBridge() + _install_listeners(b) + return b + + +panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR, + _bridge_factory, icon_spec=("D", "#9e7050")) diff --git a/rhino/elemente.py b/rhino/elemente.py new file mode 100644 index 0000000..3df4357 --- /dev/null +++ b/rhino/elemente.py @@ -0,0 +1,4775 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +elemente.py +ELEMENTE-Panel: Smart Architektur-Elemente. +Phase 1: Waende — Achsen-Linie (editierbar) + Volumen (auto-generiert). +Achse ist die Quelle der Wahrheit, Volumen wird bei jeder Achsen-Aenderung +oder Geschoss-Aenderung neu gebaut. +""" +import os +import sys +import json +import uuid +import Rhino +import Rhino.Geometry as rg +import System +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import panel_base + +PANEL_GUID_STR = "5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" + +# UserString-Keys auf Elementen +_KEY_ID = "dossier_element_id" # gemeinsame UUID Achse+Volumen +_KEY_TYPE = "dossier_element_type" # "wand_axis" | "wand_volume" +_KEY_GESCHOSS = "dossier_geschoss" +_KEY_DICKE = "dossier_dicke" # in doc-units +_KEY_UK_OVER = "dossier_uk_override" # "" = auto, sonst float +_KEY_OK_OVER = "dossier_ok_override" +_KEY_REFERENZ = "dossier_referenz" # "mid" | "left" | "right" +_KEY_DACH_NEIGUNG = "dossier_dach_neigung" # Grad als string ("30") +_KEY_DACH_EAVE = "dossier_dach_eave" # Index der Traufkante (string) +_KEY_DACH_TYP = "dossier_dach_typ" # "pult"|"sattel"|"walm"|"mansarde" +_KEY_DACH_NEIG_UNTEN = "dossier_dach_neigung_unten" # Mansarde: untere Neigung +_KEY_DACH_KNICK_H = "dossier_dach_knick_h" # Mansarde: Hoehe des Knicks +_KEY_DACH_VARIANTE = "dossier_dach_variante" # Mansarde: "walm" | "giebel" | "walm_giebel" +# Oeffnungen (Fenster/Tueren) — Source = Point auf Wand-Achse +_KEY_OEFF_TYP = "dossier_oeff_typ" # "fenster" | "tuer" +_KEY_OEFF_PARENT = "dossier_oeff_parent" # parent wand element_id +_KEY_OEFF_BREITE = "dossier_oeff_breite" +_KEY_OEFF_HOEHE = "dossier_oeff_hoehe" +_KEY_OEFF_BRUEST = "dossier_oeff_brueest" # Bruestungshoehe (nur Fenster, sonst 0) +_KEY_OEFF_RAHMEN_B = "dossier_oeff_rahmen_b" # Rahmen-Riegel-Breite (Profilbreite, in der Wandflaeche) +_KEY_OEFF_RAHMEN_TIEFE = "dossier_oeff_rahmen_tiefe" # Rahmen-Tiefe (entlang Wandnormale) +_KEY_OEFF_RAHMEN_POS = "dossier_oeff_rahmen_pos" # "aussen" | "mid" | "innen" — Lage im Wandquerschnitt +_KEY_OEFF_FLUEGEL = "dossier_oeff_fluegel" # Anzahl Fluegel (1,2,3,4) +_KEY_OEFF_SIMS_AUS = "dossier_oeff_sims_aus" # Style: "ohne"|"schmal"|"standard"|"breit" +_KEY_OEFF_SIMS_IN = "dossier_oeff_sims_in" # Style: "ohne"|"schmal"|"standard"|"breit" +_KEY_OEFF_GLAS = "dossier_oeff_glas" # "1"|"0" — sichtbare Glas-Scheibe +_KEY_OEFF_REFERENZ = "dossier_oeff_referenz" # "mid" | "links" | "rechts" — Lage des Klick-Punkts in der Oeffnung + +_OEFF_REFERENZ_OPTIONS = ("mid", "links", "rechts") + +# Treppen-spezifische Keys +_KEY_GESCHOSS_END = "dossier_geschoss_end" # Zielgeschoss-ID (Treppe) +_KEY_TREPPE_BREITE = "dossier_treppe_breite" +_KEY_TREPPE_N = "dossier_treppe_n" # Anzahl Stufen (Steigungen) +_KEY_TREPPE_REFERENZ = "dossier_treppe_referenz" # "mid"|"links"|"rechts" — Lage der Lauflinie zur Treppe +_KEY_TREPPE_MODUS = "dossier_treppe_modus" # "massiv"|"flach"|"plattenrand" +_KEY_TREPPE_LAUF_D = "dossier_treppe_lauf_d" # Lauf-Plattendicke (m) +_KEY_TREPPE_ART = "dossier_treppe_art" # "gerade"|"l"|"wendel" +_KEY_TREPPE_H_OVER = "dossier_treppe_h_over" # eigene Hoehe (m); leer = Geschoss +_KEY_TREPPE_SOLL = "dossier_treppe_soll" # JSON {s:[lo,hi,on], a:[lo,hi,on], sa:[lo,hi,on]} + +_TREPPE_SOLL_DEFAULT = { + "s": [0.15, 0.20, True], + "a": [0.21, 0.35, True], + "sa": [0.60, 0.65, True], +} + +_TREPPE_MODI = ("massiv", "flach", "plattenrand") +_TREPPE_ARTEN = ("gerade", "l", "wendel") + +# Sims-Stile (Aussen/Innen) — Dicke (Z), Auskragung (perp), Ueberhang seitlich +_OEFF_SIMS_STYLES = { + "ohne": None, + "schmal": {"dicke": 0.03, "aus": 0.08, "ueberhang": 0.03}, + "standard": {"dicke": 0.04, "aus": 0.14, "ueberhang": 0.05}, + "breit": {"dicke": 0.05, "aus": 0.22, "ueberhang": 0.06}, +} +_OEFF_RAHMEN_POS_OPTIONS = ("aussen", "mid", "innen") + + +# --- Last-Used-Defaults (sticky, session-life) ------------------------------ +# Speichert die letzten Werte (Dicke, Referenz, Modus, Neigung), damit der +# naechste Create-Befehl mit denselben Defaults startet. Sticky ueberlebt +# Doc-Wechsel, aber NICHT Rhino-Restart — was passt: "ich hab gerade 0.30 +# fuer eine Wand benutzt, neue Wand soll auch 0.30 sein". + +def _last(key, default): + return sc.sticky.get("elemente_last_" + key, default) + + +def _save_last(**kwargs): + for k, v in kwargs.items(): + sc.sticky["elemente_last_" + k] = v + + +# --- Geschoss-Lookup -------------------------------------------------------- + +def _load_geschosse(doc): + """Liest die Geschoss/Ebenen-Liste aus doc.Strings (vom Ebenen-Manager).""" + raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or \ + doc.Strings.GetValue("dossier_ebenen") + if not raw: return [] + try: + data = json.loads(raw) + return data if isinstance(data, list) else [] + except Exception: + return [] + + +def _geschoss_by_id(doc, gid): + if not gid: return None + for e in _load_geschosse(doc): + if isinstance(e, dict) and e.get("id") == gid: + return e + return None + + +def _active_geschoss_id(doc): + """Liefert die ID des aktuell aktiven Geschosses (= im Ebenen-Manager + blau hervorgehoben). Falls keins gesetzt oder das aktive keine + Geschoss-Ebene ist (z.B. Schnitt/Ansicht), wird das erste echte + Geschoss zurueckgegeben.""" + try: + active = doc.Strings.GetValue("dossier_active_id") or "" + except Exception: + active = "" + geschosse = [g for g in _load_geschosse(doc) + if isinstance(g, dict) and g.get("isGeschoss")] + if active and any(g.get("id") == active for g in geschosse): + return active + return geschosse[0].get("id") if geschosse else "" + + +def _active_geschoss_name(doc): + """Name des aktiven Geschosses fuer UI-Anzeige.""" + gid = _active_geschoss_id(doc) + g = _geschoss_by_id(doc, gid) + return g.get("name", "") if g else "" + + +def _resolve_uk_ok(doc, gid, uk_over, ok_over): + """Wand: UK = OKFF, OK = OKFF + Hoehe (Standard fuer Geschoss-volle Wand).""" + g = _geschoss_by_id(doc, gid) + if g is None: + uk = float(uk_over) if uk_over not in (None, "") else 0.0 + ok = float(ok_over) if ok_over not in (None, "") else 3.0 + return uk, ok + okff = float(g.get("okff", 0.0)) + hoehe = float(g.get("hoehe", 3.0)) + auto_uk = okff + auto_ok = okff + hoehe + uk = float(uk_over) if uk_over not in (None, "") else auto_uk + ok = float(ok_over) if ok_over not in (None, "") else auto_ok + return uk, ok + + +def _resolve_decke_z(doc, gid, dicke, uk_over, ok_over): + """Decke: OK = OKFF des verknuepften Geschosses (= Bodenkante = 0.00 + relativ zum Geschoss). UK = OK - dicke (Decke geht NACH UNTEN). OK ist + der natuerliche Fixpunkt: aendert sich die Dicke, wandert UK mit. + + Override-Logik: + - Nur OK_override gesetzt → OK = override, UK = OK - dicke + - Nur UK_override gesetzt → UK = override, OK = UK + dicke + - Beide gesetzt → beide literal""" + g = _geschoss_by_id(doc, gid) + okff = float(g.get("okff", 0.0)) if g else 0.0 + auto_ok = okff + has_ok = ok_over not in (None, "") + has_uk = uk_over not in (None, "") + if has_ok and has_uk: + return float(uk_over), float(ok_over) + if has_ok: + ok = float(ok_over) + return ok - float(dicke), ok + if has_uk: + uk = float(uk_over) + return uk, uk + float(dicke) + # Beide auto + return auto_ok - float(dicke), auto_ok + + +# --- Layer-Pfade ------------------------------------------------------------ + +def _find_ebene_sublayer_name(doc, keywords, default_code, default_name, + default_color="#888888", default_lw=0.35): + """Findet aus der Ebenen-Liste den ersten Sublayer der einem der Keywords + entspricht. Wenn nicht gefunden, wird der Sublayer mit den Default-Werten + AUTOMATISCH in die Ebenen-Liste eingetragen (damit er auch im Ebenen- + Manager-UI erscheint) und der Rhinopanel-Bridge ein State-Refresh + getriggert. Ergebnis: 'CODE_NAME' wie 'WAENDE'.""" + raw = doc.Strings.GetValue("dossier_ebenen") + ebenen = [] + if raw: + try: + data = json.loads(raw) + if isinstance(data, list): ebenen = data + except Exception as ex: + print("[ELEMENTE] sublayer-lookup:", ex) + # 1) Per Keyword in der Liste suchen + for e in ebenen: + if not isinstance(e, dict): continue + name = (e.get("name") or "") + low = name.lower() + for kw in keywords: + if kw in low: + return "{}_{}".format(e.get("code", default_code), + name or default_name) + # 2) Auto-Add: weder Keyword noch Code vorhanden → Eintrag anlegen + if ebenen and not any(isinstance(e, dict) and e.get("code") == default_code + for e in ebenen): + ebenen.append({ + "code": default_code, "name": default_name, + "color": default_color, "lw": default_lw, + "visible": True, "locked": False, + }) + try: + doc.Strings.SetString("dossier_ebenen", + json.dumps(ebenen, ensure_ascii=False)) + print("[ELEMENTE] Ebene '{}_{}' automatisch hinzugefuegt".format( + default_code, default_name)) + # Ebenen-Manager UI mit-informieren + b = sc.sticky.get("ebenen_bridge_ref") \ + or sc.sticky.get("ebenen_bridge") \ + or sc.sticky.get("rhinopanel_bridge") + if b is not None and hasattr(b, "_send_state"): + try: b._send_state() + except Exception: pass + except Exception as ex: + print("[ELEMENTE] Auto-Add fehler:", ex) + return "{}_{}".format(default_code, default_name) + + +def _layer_path_axis(doc, geschoss_name): + """Wand-Achse + Volumen — Sublayer 'WÄNDE' (Code 20).""" + sub = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"], + "20", "WÄNDE", + default_color="#0a0a0a", default_lw=0.50) + return "{}::{}".format(geschoss_name, sub) + + +def _layer_path_volume(doc, geschoss_name): + return _layer_path_axis(doc, geschoss_name) + + +def _layer_path_decke(doc, geschoss_name): + """Decken-Outline + Volumen — Sublayer 'DECKEN' (Code 30).""" + sub = _find_ebene_sublayer_name(doc, ["decke"], "30", "DECKEN", + default_color="#605850", default_lw=0.35) + return "{}::{}".format(geschoss_name, sub) + + +def _layer_path_dach(doc, geschoss_name): + """Dach-Outline + Volumen — Sublayer 'DÄCHER' (Code 31).""" + sub = _find_ebene_sublayer_name(doc, ["dach", "däch", "daech"], + "31", "DÄCHER", + default_color="#7a4a3a", default_lw=0.35) + return "{}::{}".format(geschoss_name, sub) + + +def _layer_path_treppe(doc, geschoss_name): + """Treppen-Lauflinie + Volumen — Sublayer 'TREPPEN' (Code 40).""" + sub = _find_ebene_sublayer_name(doc, ["trepp"], "40", "TREPPEN", + default_color="#a08040", default_lw=0.35) + return "{}::{}".format(geschoss_name, sub) + + +def _ensure_layer(doc, path): + """Stellt sicher, dass ein Layer-Pfad existiert. Liefert Layer-Index.""" + idx = doc.Layers.FindByFullPath(path, -1) + if idx >= 0: return idx + # Schrittweise anlegen + parts = path.split("::") + parent_id = System.Guid.Empty + cur_path = "" + for part in parts: + cur_path = part if not cur_path else (cur_path + "::" + part) + idx = doc.Layers.FindByFullPath(cur_path, -1) + if idx < 0: + from Rhino.DocObjects import Layer + layer = Layer() + layer.Name = part + if parent_id != System.Guid.Empty: + layer.ParentLayerId = parent_id + idx = doc.Layers.Add(layer) + parent_id = doc.Layers[idx].Id + return idx + + +# --- Wall-Konstruktion ------------------------------------------------------ + +def _make_rectangle_preview(c1): + """Preview: 4 gruene Kanten des Rechtecks waehrend des Ziehens.""" + import System.Drawing as SD + color = SD.Color.FromArgb(255, 90, 200, 90) + def handler(sender, e): + try: + cx, cy = e.CurrentPoint.X, e.CurrentPoint.Y + p1 = rg.Point3d(c1.X, c1.Y, 0) + p2 = rg.Point3d(cx, c1.Y, 0) + p3 = rg.Point3d(cx, cy, 0) + p4 = rg.Point3d(c1.X, cy, 0) + for a, b in ((p1, p2), (p2, p3), (p3, p4), (p4, p1)): + e.Display.DrawLine(a, b, color, 2) + except Exception: pass + return handler + + +def _make_rect3pt_preview(c1, c2): + """Preview fuer 3-Punkt-Rechteck. c2=None waehrend Sammlung der zweiten + Ecke (zeige Linie c1→Maus), sonst zeige rotiertes Rechteck.""" + import System.Drawing as SD + color = SD.Color.FromArgb(255, 90, 200, 90) + p1 = rg.Point3d(c1.X, c1.Y, 0) + def handler(sender, e): + try: + cur = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) + if c2 is None: + e.Display.DrawLine(p1, cur, color, 2) + return + p2 = rg.Point3d(c2.X, c2.Y, 0) + ex = p2.X - p1.X; ey = p2.Y - p1.Y + edge_len = (ex * ex + ey * ey) ** 0.5 + if edge_len < 1e-9: + e.Display.DrawLine(p1, p2, color, 2); return + px = -ey / edge_len; py = ex / edge_len + d = (cur.X - p2.X) * px + (cur.Y - p2.Y) * py + p3 = rg.Point3d(p2.X + d * px, p2.Y + d * py, 0) + p4 = rg.Point3d(p1.X + d * px, p1.Y + d * py, 0) + for a, b in ((p1, p2), (p2, p3), (p3, p4), (p4, p1)): + e.Display.DrawLine(a, b, color, 2) + except Exception: pass + return handler + + +def _make_circle_preview(center): + """Preview: Kreis vom Mittelpunkt zum Mauspunkt.""" + import System.Drawing as SD + color = SD.Color.FromArgb(255, 90, 200, 90) + cen = rg.Point3d(center.X, center.Y, 0) + def handler(sender, e): + try: + cur = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) + r = cen.DistanceTo(cur) + if r <= 1e-9: return + try: + e.Display.DrawCircle(rg.Circle(rg.Plane.WorldXY, cen, r), color, 2) + except Exception: + # Fallback: NurbsCurve zeichnen + e.Display.DrawCurve(rg.Circle(rg.Plane.WorldXY, cen, r).ToNurbsCurve(), + color, 2) + except Exception: pass + return handler + + +def _collect_rectangle(doc, c1): + """Achsen-aligned Rechteck aus 2 diagonalen Ecken. Liefert geschlossene + PolylineCurve in XY-Ebene auf Z=0.""" + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception: return None + gp = ric.GetPoint() + gp.SetCommandPrompt("Gegenueberliegende Ecke") + try: gp.SetBasePoint(c1, True) + except Exception: pass + try: gp.DynamicDraw += _make_rectangle_preview(c1) + except Exception: pass + res = gp.Get() + if res != GetResult.Point: return None + c2 = gp.Point() + pts = [ + rg.Point3d(c1.X, c1.Y, 0), + rg.Point3d(c2.X, c1.Y, 0), + rg.Point3d(c2.X, c2.Y, 0), + rg.Point3d(c1.X, c2.Y, 0), + rg.Point3d(c1.X, c1.Y, 0), + ] + return rg.PolylineCurve(rg.Polyline(pts)) + + +def _collect_rectangle_3pt(doc, c1): + """3-Punkt-Rechteck: c1 = erste Ecke, c2 = Ende der ersten Kante (definiert + Richtung), c3 = Punkt auf der gegenueberliegenden Seite (definiert Hoehe). + Erzeugt rotiertes Rechteck.""" + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception: return None + gp = ric.GetPoint() + gp.SetCommandPrompt("Ende der ersten Kante") + try: gp.SetBasePoint(c1, True) + except Exception: pass + try: gp.DynamicDraw += _make_rect3pt_preview(c1, None) + except Exception: pass + res = gp.Get() + if res != GetResult.Point: return None + c2 = gp.Point() + gp = ric.GetPoint() + gp.SetCommandPrompt("Hoehe (Punkt auf gegenueberliegender Seite)") + try: gp.SetBasePoint(c2, True) + except Exception: pass + try: gp.DynamicDraw += _make_rect3pt_preview(c1, c2) + except Exception: pass + res = gp.Get() + if res != GetResult.Point: return None + c3 = gp.Point() + ex = c2.X - c1.X + ey = c2.Y - c1.Y + edge_len = (ex * ex + ey * ey) ** 0.5 + if edge_len < 1e-9: return None + # Perpendikular in XY (links der edge-Richtung) + px = -ey / edge_len + py = ex / edge_len + # Signierte Distanz von c3 zur Edge (c1-c2) + d = (c3.X - c2.X) * px + (c3.Y - c2.Y) * py + p1 = rg.Point3d(c1.X, c1.Y, 0) + p2 = rg.Point3d(c2.X, c2.Y, 0) + p3 = rg.Point3d(c2.X + d * px, c2.Y + d * py, 0) + p4 = rg.Point3d(c1.X + d * px, c1.Y + d * py, 0) + return rg.PolylineCurve(rg.Polyline([p1, p2, p3, p4, p1])) + + +def _collect_circle(doc, center): + """Kreis aus Mittelpunkt + Radiuspunkt. Liefert NurbsCurve.""" + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception: return None + gp = ric.GetPoint() + gp.SetCommandPrompt("Radiuspunkt") + try: gp.SetBasePoint(center, True) + except Exception: pass + try: gp.DynamicDraw += _make_circle_preview(center) + except Exception: pass + res = gp.Get() + if res != GetResult.Point: return None + rp = gp.Point() + cen = rg.Point3d(center.X, center.Y, 0) + rad_pt = rg.Point3d(rp.X, rp.Y, 0) + radius = cen.DistanceTo(rad_pt) + if radius <= 1e-9: return None + return rg.Circle(rg.Plane.WorldXY, cen, radius).ToNurbsCurve() + + +def _make_decke_preview_handler(committed_points): + """Live-Preview waehrend Decken-Outline gezeichnet wird: gesetzte Segmente + + Rubberband + gestrichelte Schliessungs-Linie zurueck zum Startpunkt.""" + import System.Drawing as SD + color_line = SD.Color.FromArgb(255, 90, 200, 90) + color_close = SD.Color.FromArgb(180, 150, 230, 150) + color_node = SD.Color.FromArgb(255, 255, 255, 255) + def handler(sender, e): + try: + cur = e.CurrentPoint + cur_xy = rg.Point3d(cur.X, cur.Y, 0) + pts = list(committed_points) + [cur_xy] + for i in range(len(pts) - 1): + e.Display.DrawLine(pts[i], pts[i + 1], color_line, 2) + # Schliessungs-Hinweis: gestrichelte Linie zurueck zum Startpunkt + if len(committed_points) >= 2: + try: + e.Display.DrawDottedLine(cur_xy, committed_points[0], color_close) + except Exception: + e.Display.DrawLine(cur_xy, committed_points[0], color_close, 1) + for pp in committed_points: + try: e.Display.DrawPoint(pp, color_node) + except Exception: pass + except Exception: + pass + return handler + + +def _draw_axis_with_offsets(display, axis_curve, dicke, referenz, + color_axis, color_edge): + """Zeichnet eine Wand-Achse + ihre Offset-Kanten (Aussenkanten der Wand). + Wird von allen Wand-Preview-Handlern wiederverwendet.""" + try: display.DrawCurve(axis_curve, color_axis, 2) + except Exception: pass + plane = rg.Plane.WorldXY + tol = 0.001 + half = float(dicke) / 2.0 + if referenz == "left": + offsets = [0.0, -float(dicke)] + elif referenz == "right": + offsets = [+float(dicke), 0.0] + else: + offsets = [+half, -half] + for d in offsets: + try: + if abs(d) < 1e-9: + display.DrawCurve(axis_curve, color_edge, 1) + else: + result = axis_curve.Offset(plane, d, tol, + rg.CurveOffsetCornerStyle.Sharp) + if result: + for c in result: + display.DrawCurve(c, color_edge, 1) + except Exception: pass + + +def _make_treppe_preview_handler(p0, breite, referenz, n_stufen, + fixed_length=None, + min_length=None, max_length=None): + """Live-Preview fuer die gerade Treppe waehrend der Lauflinien-Wahl. + Zeichnet: Lauflinie (Mitte), die zwei Aussenkanten (je nach Referenz) + sowie kurze Querstriche an jeder Setzstufen-Position. + + Laengen-Steuerung (von hoechster zu niedrigster Prio): + - `fixed_length`: Mausvektor wird genau auf diese Laenge reskaliert + - `min_length` / `max_length`: Mausvektor wird in dieser Range + geclampt (frei innerhalb, Stop bei den Grenzen) + - sonst: Mausvektor wird unveraendert benutzt""" + import System.Drawing as SD + color_axis = SD.Color.FromArgb(255, 90, 200, 90) + color_edge = SD.Color.FromArgb(180, 120, 220, 120) + color_step = SD.Color.FromArgb(200, 200, 240, 120) + p0_xy = rg.Point3d(p0.X, p0.Y, 0) + N = max(2, int(n_stufen)) + b = float(breite) + if referenz == "links": + perp_lo, perp_hi = 0.0, -b + elif referenz == "rechts": + perp_lo, perp_hi = 0.0, +b + else: + perp_lo, perp_hi = -b * 0.5, +b * 0.5 + + def handler(sender, e): + try: + mouse = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) + tan_vec = rg.Vector3d(mouse.X - p0_xy.X, mouse.Y - p0_xy.Y, 0) + mouse_dist = tan_vec.Length + if mouse_dist < 1e-4: return + tan_vec.Unitize() + # Bei Regel-Modus: Endpunkt entweder fix oder in einer Range. + if fixed_length is not None and fixed_length > 1e-4: + L = float(fixed_length) + cur = rg.Point3d(p0_xy.X + tan_vec.X * L, + p0_xy.Y + tan_vec.Y * L, 0) + elif min_length is not None or max_length is not None: + lo = float(min_length) if min_length is not None else 1e-4 + hi = float(max_length) if max_length is not None else 1e9 + L = mouse_dist + if L < lo: L = lo + if L > hi: L = hi + cur = rg.Point3d(p0_xy.X + tan_vec.X * L, + p0_xy.Y + tan_vec.Y * L, 0) + else: + L = mouse_dist + cur = mouse + perp = rg.Vector3d(-tan_vec.Y, tan_vec.X, 0) + + # Lauflinie (Mittel-Achse) gruen + try: e.Display.DrawLine(p0_xy, cur, color_axis, 2) + except Exception: pass + + # Aussenkanten der Treppe (zwei Parallelen je nach Referenz) + def edge_at(perp_off): + ax = rg.Point3d(p0_xy.X + perp.X * perp_off, + p0_xy.Y + perp.Y * perp_off, 0) + bx = rg.Point3d(cur.X + perp.X * perp_off, + cur.Y + perp.Y * perp_off, 0) + try: e.Display.DrawLine(ax, bx, color_edge, 1) + except Exception: pass + edge_at(perp_lo); edge_at(perp_hi) + + # Querstriche an jeder Setzstufen-Position (N Auftritte) + A = L / max(1, N) + for k in range(1, N + 1): + x = k * A + if x > L + 1e-6: break + mid = rg.Point3d(p0_xy.X + tan_vec.X * x, + p0_xy.Y + tan_vec.Y * x, 0) + a = rg.Point3d(mid.X + perp.X * perp_lo, + mid.Y + perp.Y * perp_lo, 0) + bp = rg.Point3d(mid.X + perp.X * perp_hi, + mid.Y + perp.Y * perp_hi, 0) + try: e.Display.DrawLine(a, bp, color_step, 1) + except Exception: pass + except Exception: pass + return handler + + +def _make_treppe_wendel_preview(center, start, breite, referenz, n_stufen, + total_h=None, soll=None, regel_mode="frei"): + """Live-Preview fuer den 3. Klick einer Wendeltreppe. Zeichnet: + Mittelpunkt-Lauflinie + alle N Keile fuer die aktuelle End-Position. + + Bei `regel_mode == "regel"` wird der Sweep auf einen gueltigen Bereich + geclampt — die Richtung kommt aus der Maus, die Drehung wird auf den + Soll-A-Wert beschraenkt. So bleiben Auftritt + 2S+A im Soll.""" + import System.Drawing as SD + import math + color_axis = SD.Color.FromArgb(255, 90, 200, 90) + color_edge = SD.Color.FromArgb(180, 120, 220, 120) + color_step = SD.Color.FromArgb(200, 200, 240, 120) + cx, cy = center.X, center.Y + sx, sy = start.X, start.Y + r_click = math.sqrt((sx - cx) ** 2 + (sy - cy) ** 2) + if r_click < 0.05: r_click = 0.05 + r_inner, r_outer = _wendel_radii(r_click, breite, referenz) + a_start_fixed = math.atan2(sy - cy, sx - cx) + N = max(2, int(n_stufen)) + + def handler(sender, e): + try: + mouse = e.CurrentPoint + cross_z = ((sx - cx) * (mouse.Y - cy) + - (sy - cy) * (mouse.X - cx)) + sweep_sign = 1.0 if cross_z >= 0 else -1.0 + a_end_raw = math.atan2(mouse.Y - cy, mouse.X - cx) + delta = a_end_raw - a_start_fixed + if sweep_sign > 0: + while delta < 0: delta += 2.0 * math.pi + else: + while delta > 0: delta -= 2.0 * math.pi + if abs(delta) < 0.02: return + # Clamp Sweep im Regel-Modus — Auftritt-Soll wird ueber die + # GANZE Trittbreite (innen + aussen) erzwungen. + if regel_mode == "regel" and total_h is not None and soll is not None: + try: + s_lo, s_hi = _wendel_sweep_range( + r_click, breite, referenz, N, total_h, soll) + raw = abs(delta) + if raw < s_lo: clamped = s_lo + elif raw > s_hi: clamped = s_hi + else: clamped = raw + delta = clamped * (1.0 if delta >= 0 else -1.0) + except Exception: pass + da = delta / N + + # Lauflinie center→mouse + try: + e.Display.DrawLine(rg.Point3d(cx, cy, 0), + rg.Point3d(mouse.X, mouse.Y, 0), + color_axis, 1) + except Exception: pass + + # Alle N Keile als Quad-Linien + for k in range(N): + a0 = a_start_fixed + k * da + a1 = a_start_fixed + (k + 1) * da + pi0 = rg.Point3d(cx + r_inner * math.cos(a0), + cy + r_inner * math.sin(a0), 0) + po0 = rg.Point3d(cx + r_outer * math.cos(a0), + cy + r_outer * math.sin(a0), 0) + pi1 = rg.Point3d(cx + r_inner * math.cos(a1), + cy + r_inner * math.sin(a1), 0) + po1 = rg.Point3d(cx + r_outer * math.cos(a1), + cy + r_outer * math.sin(a1), 0) + # Riser bei a0 (radial-Linie) + try: e.Display.DrawLine(pi0, po0, color_step, 1) + except Exception: pass + # Inner & outer "Bogen" (linear approximiert) + try: e.Display.DrawLine(pi0, pi1, color_edge, 1) + except Exception: pass + try: e.Display.DrawLine(po0, po1, color_edge, 1) + except Exception: pass + # Letzter Riser bei alpha_final + a_f = a_start_fixed + delta + pif = rg.Point3d(cx + r_inner * math.cos(a_f), + cy + r_inner * math.sin(a_f), 0) + pof = rg.Point3d(cx + r_outer * math.cos(a_f), + cy + r_outer * math.sin(a_f), 0) + try: e.Display.DrawLine(pif, pof, color_step, 1) + except Exception: pass + + # Live-Label: Stufen, Sweep, Auftritt an Innen/Lauf/Aussen + try: + deg = abs(delta) * 180.0 / math.pi + A_in = abs(da) * r_inner + A_lauf = abs(da) * r_click + A_out = abs(da) * r_outer + lbl = "St {} | {:.0f}° | A i/l/a: {:.2f}/{:.2f}/{:.2f}".format( + N, deg, A_in, A_lauf, A_out) + if regel_mode == "regel": + lbl += " (Regel)" + e.Display.DrawDot(rg.Point3d(mouse.X, mouse.Y, 0), lbl) + except Exception: pass + except Exception: pass + return handler + + +def _make_treppe_l_corner_preview(p0, breite, referenz, total_n, total_h): + """Preview fuer den 2. Klick einer L-Treppe (Podest-Eck). Zeigt: + - Lauflinie + Aussenkanten + - Step-Lines an A_opt-Abstaenden (zeigt wo jeder Tritt landet) + - Live-Label mit N1 (Stufen vor Podest) und N2 (nach Podest) + """ + import System.Drawing as SD + color_axis = SD.Color.FromArgb(255, 90, 200, 90) + color_edge = SD.Color.FromArgb(180, 120, 220, 120) + color_step = SD.Color.FromArgb(200, 200, 240, 120) + p0_xy = rg.Point3d(p0.X, p0.Y, 0) + half_b = float(breite) * 0.5 + if referenz == "links": + perp_lo, perp_hi = 0.0, -float(breite) + elif referenz == "rechts": + perp_lo, perp_hi = 0.0, +float(breite) + else: + perp_lo, perp_hi = -half_b, +half_b + + # A_opt aus Soll-Schrittmass 0.63 - 2*S, geclampt auf erlaubten Bereich + S = float(total_h) / max(1, int(total_n)) + A_opt = 0.63 - 2.0 * S + if A_opt < 0.21: A_opt = 0.21 + if A_opt > 0.35: A_opt = 0.35 + + def handler(sender, e): + try: + mouse = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) + tan_vec = rg.Vector3d(mouse.X - p0_xy.X, mouse.Y - p0_xy.Y, 0) + L = tan_vec.Length + if L < 1e-4: return + tan_vec.Unitize() + perp = rg.Vector3d(-tan_vec.Y, tan_vec.X, 0) + + try: e.Display.DrawLine(p0_xy, mouse, color_axis, 2) + except Exception: pass + + def edge_at(perp_off): + a = rg.Point3d(p0_xy.X + perp.X * perp_off, + p0_xy.Y + perp.Y * perp_off, 0) + b = rg.Point3d(mouse.X + perp.X * perp_off, + mouse.Y + perp.Y * perp_off, 0) + try: e.Display.DrawLine(a, b, color_edge, 1) + except Exception: pass + edge_at(perp_lo); edge_at(perp_hi) + + # N1 = Stufen die in Run 1 passen (effektive Laenge = L - half_b + # weil Podest die Haelfte einnimmt). N2 = restliche Stufen. + eff_L1 = max(0.0, L - half_b) + N1 = max(0, int(round(eff_L1 / A_opt))) + N1 = min(N1, int(total_n) - 1) + N2 = max(0, int(total_n) - N1) + + # Step-Lines an N1 Positionen + for k in range(1, N1 + 1): + x = k * A_opt + if x > L + 1e-6: break + mid = rg.Point3d(p0_xy.X + tan_vec.X * x, + p0_xy.Y + tan_vec.Y * x, 0) + a = rg.Point3d(mid.X + perp.X * perp_lo, + mid.Y + perp.Y * perp_lo, 0) + bp = rg.Point3d(mid.X + perp.X * perp_hi, + mid.Y + perp.Y * perp_hi, 0) + try: e.Display.DrawLine(a, bp, color_step, 1) + except Exception: pass + + # Live-Label am Mauspunkt + try: + e.Display.DrawDot(mouse, "Vor Podest: {} | Nach: {}".format(N1, N2)) + except Exception: pass + except Exception: pass + return handler + + +def _make_spline_preview_handler(committed_points, dicke, referenz): + """Preview fuer Spline-Wand: interpolierter NURBS durch committed + Maus.""" + import System.Drawing as SD + color_axis = SD.Color.FromArgb(255, 90, 200, 90) + color_edge = SD.Color.FromArgb(180, 120, 220, 120) + color_node = SD.Color.FromArgb(255, 255, 255, 255) + def handler(sender, e): + try: + cur_xy = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) + pts = list(committed_points) + [cur_xy] + if len(pts) < 2: return + axis = None + if len(pts) == 2: + axis = rg.LineCurve(pts[0], pts[1]) + else: + try: axis = rg.Curve.CreateInterpolatedCurve(pts, 3) + except Exception: + axis = rg.PolylineCurve(rg.Polyline(pts)) + if axis is None: return + _draw_axis_with_offsets(e.Display, axis, dicke, referenz, + color_axis, color_edge) + for pp in committed_points: + try: e.Display.DrawPoint(pp, color_node) + except Exception: pass + except Exception: pass + return handler + + +def _make_arc_preview_handler(p0, p_mid, dicke, referenz): + """Preview fuer Bogen-Wand. p_mid=None waehrend Sammlung des Mittelpunkts + (nur Linie zeigen), sonst echter Bogen p0→p_mid→Maus.""" + import System.Drawing as SD + color_axis = SD.Color.FromArgb(255, 90, 200, 90) + color_edge = SD.Color.FromArgb(180, 120, 220, 120) + color_node = SD.Color.FromArgb(255, 255, 255, 255) + p0_xy = rg.Point3d(p0.X, p0.Y, 0) + def handler(sender, e): + try: + cur_xy = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) + try: e.Display.DrawPoint(p0_xy, color_node) + except Exception: pass + if p_mid is None: + # Phase 1: Mittelpunkt — Rubberband-Linie + e.Display.DrawLine(p0_xy, cur_xy, color_axis, 2) + return + mid_xy = rg.Point3d(p_mid.X, p_mid.Y, 0) + try: e.Display.DrawPoint(mid_xy, color_node) + except Exception: pass + # Phase 2: Endpunkt — Bogen + Offsets + try: arc = rg.Arc(p0_xy, mid_xy, cur_xy) + except Exception: return + if not arc.IsValid: return + axis = rg.ArcCurve(arc) + _draw_axis_with_offsets(e.Display, axis, dicke, referenz, + color_axis, color_edge) + except Exception: pass + return handler + + +def _make_rectangle_wall_preview_handler(c1, dicke, referenz): + """Preview fuer Wand-Rechteck: 4 Linien-Achsen + ihre Offsets.""" + import System.Drawing as SD + color_axis = SD.Color.FromArgb(255, 90, 200, 90) + color_edge = SD.Color.FromArgb(180, 120, 220, 120) + c1_xy = rg.Point3d(c1.X, c1.Y, 0) + def handler(sender, e): + try: + cx = e.CurrentPoint.X + cy = e.CurrentPoint.Y + p1 = c1_xy + p2 = rg.Point3d(cx, c1_xy.Y, 0) + p3 = rg.Point3d(cx, cy, 0) + p4 = rg.Point3d(c1_xy.X, cy, 0) + corners = [p1, p2, p3, p4, p1] + for i in range(4): + axis = rg.LineCurve(corners[i], corners[i + 1]) + _draw_axis_with_offsets(e.Display, axis, dicke, referenz, + color_axis, color_edge) + except Exception: pass + return handler + + +def _make_preview_handler(committed_points, dicke, referenz): + """Preview fuer Polylinie-Wand: gesetzte Punkte + Rubberband + Wand-Kanten.""" + import System.Drawing as SD + color_axis = SD.Color.FromArgb(255, 90, 200, 90) + color_edge = SD.Color.FromArgb(180, 120, 220, 120) + color_node = SD.Color.FromArgb(255, 255, 255, 255) + def handler(sender, e): + try: + cur_xy = rg.Point3d(e.CurrentPoint.X, e.CurrentPoint.Y, 0) + pts = list(committed_points) + [cur_xy] + if len(pts) < 2: return + axis = (rg.LineCurve(pts[0], pts[1]) if len(pts) == 2 + else rg.PolylineCurve(rg.Polyline(pts))) + _draw_axis_with_offsets(e.Display, axis, dicke, referenz, + color_axis, color_edge) + for pp in committed_points: + try: e.Display.DrawPoint(pp, color_node) + except Exception: pass + except Exception: pass + return handler + + +def _make_axis_geometry(p0, p1): + """2D-Wandlinie: Linie auf der UK-Hoehe. Aber damit der User die + Achse frei editieren kann, behalten wir die Linie in der XY-Ebene + auf Z=0 — UK/OK kommen aus dem Geschoss-Lookup, nicht aus der Linie. + """ + return rg.LineCurve(rg.Point3d(p0.X, p0.Y, 0), rg.Point3d(p1.X, p1.Y, 0)) + + +def _make_volume_from_line(p0_xy, p1_xy, dicke, uk, ok): + """Fallback: gerade Wand aus zwei XY-Punkten.""" + p0 = rg.Point3d(p0_xy.X, p0_xy.Y, uk) + p1 = rg.Point3d(p1_xy.X, p1_xy.Y, uk) + direction = rg.Vector3d(p1 - p0) + if direction.Length < 1e-9: return None + direction.Unitize() + normal = rg.Vector3d(-direction.Y, direction.X, 0) + half = dicke / 2.0 + a = p0 + normal * half + b = p0 - normal * half + c = p1 - normal * half + d = p1 + normal * half + poly = rg.Polyline([a, b, c, d, a]) + profile = rg.PolylineCurve(poly) + height = ok - uk + if height <= 0: return None + extrusion = rg.Extrusion.Create(profile, height, True) + if extrusion is None: return None + return extrusion.ToBrep() + + +def _offset_curve(curve, plane, distance, tol): + """Curve.Offset-Wrapper der distance=0 als reine Kopie behandelt.""" + if abs(distance) < 1e-9: + return [curve.DuplicateCurve()] + try: + result = curve.Offset(plane, distance, tol, rg.CurveOffsetCornerStyle.Sharp) + if result is None or len(result) == 0: + return None + return list(result) + except Exception: + return None + + +def _make_volume_geometry(axis_curve, dicke, uk, ok, referenz="mid"): + """Baut die Wand-Brep aus einer beliebigen Achsen-Kurve. + referenz: 'mid' = Achse mittig, 'left'/'right' = Achse auf einer Aussen- + kante (Walking-Richtung der Kurve).""" + if not isinstance(axis_curve, rg.Curve): return None + dicke = float(dicke) + if dicke <= 0: return None + height = float(ok) - float(uk) + if height <= 0: return None + + # Offsets bestimmen — Summe muss IMMER dicke sein + half = dicke / 2.0 + if referenz == "left": + d_left, d_right = 0.0, -dicke + elif referenz == "right": + d_left, d_right = +dicke, 0.0 + else: # mid + d_left, d_right = +half, -half + + plane = rg.Plane.WorldXY + tol = 0.001 + left = _offset_curve(axis_curve, plane, d_left, tol) + right = _offset_curve(axis_curve, plane, d_right, tol) + if not left or not right: + return _make_volume_from_line(axis_curve.PointAtStart, + axis_curve.PointAtEnd, dicke, uk, ok) + L = left[0] + R = right[0] + try: + R.Reverse() + cap_start = rg.LineCurve(L.PointAtEnd, R.PointAtStart) + cap_end = rg.LineCurve(R.PointAtEnd, L.PointAtStart) + joined = rg.Curve.JoinCurves([L, cap_start, R, cap_end], tol) + except Exception: + joined = None + if not joined or len(joined) == 0 or not joined[0].IsClosed: + return _make_volume_from_line(axis_curve.PointAtStart, + axis_curve.PointAtEnd, dicke, uk, ok) + profile = joined[0].DuplicateCurve() + if abs(uk) > 1e-9: + profile.Transform(rg.Transform.Translation(0, 0, uk)) + extrusion = rg.Extrusion.Create(profile, height, True) + if extrusion is None: return None + return extrusion.ToBrep() + + +def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, + referenz="mid", neigung=None, eave_idx=None, dach_typ=None, + neigung_unten=None, knick_h=None, dach_variante=None, + oeff_typ=None, oeff_parent=None, oeff_breite=None, + oeff_hoehe=None, oeff_brueest=None, + oeff_rahmen_b=None, oeff_rahmen_tiefe=None, oeff_rahmen_pos=None, + oeff_fluegel=None, + oeff_sims_aus=None, oeff_sims_in=None, oeff_glas=None, + oeff_referenz=None, + geschoss_end=None, treppe_breite=None, + treppe_n=None, treppe_referenz=None, + treppe_modus=None, treppe_lauf_d=None, treppe_art=None, + treppe_h_over=None, treppe_soll=None): + """User-Strings auf die Object-Attributes setzen.""" + obj_attrs.SetUserString(_KEY_ID, wall_id) + obj_attrs.SetUserString(_KEY_TYPE, type_) + obj_attrs.SetUserString(_KEY_GESCHOSS, geschoss or "") + obj_attrs.SetUserString(_KEY_DICKE, "{:.6f}".format(float(dicke))) + obj_attrs.SetUserString(_KEY_UK_OVER, "" if uk_over in (None, "") else "{:.6f}".format(float(uk_over))) + obj_attrs.SetUserString(_KEY_OK_OVER, "" if ok_over in (None, "") else "{:.6f}".format(float(ok_over))) + obj_attrs.SetUserString(_KEY_REFERENZ, referenz if referenz in ("mid", "left", "right") else "mid") + if neigung is not None: + obj_attrs.SetUserString(_KEY_DACH_NEIGUNG, "{:.4f}".format(float(neigung))) + if eave_idx is not None: + obj_attrs.SetUserString(_KEY_DACH_EAVE, "{}".format(int(eave_idx))) + if dach_typ is not None and dach_typ in ("pult", "sattel", "walm", "mansarde"): + obj_attrs.SetUserString(_KEY_DACH_TYP, dach_typ) + if neigung_unten is not None: + obj_attrs.SetUserString(_KEY_DACH_NEIG_UNTEN, "{:.4f}".format(float(neigung_unten))) + if knick_h is not None: + obj_attrs.SetUserString(_KEY_DACH_KNICK_H, "{:.6f}".format(float(knick_h))) + if dach_variante is not None and dach_variante in ("walm", "giebel", "walm_giebel"): + obj_attrs.SetUserString(_KEY_DACH_VARIANTE, dach_variante) + if oeff_typ is not None and oeff_typ in ("fenster", "tuer"): + obj_attrs.SetUserString(_KEY_OEFF_TYP, oeff_typ) + if oeff_parent is not None: + obj_attrs.SetUserString(_KEY_OEFF_PARENT, str(oeff_parent)) + if oeff_breite is not None: + obj_attrs.SetUserString(_KEY_OEFF_BREITE, "{:.4f}".format(float(oeff_breite))) + if oeff_hoehe is not None: + obj_attrs.SetUserString(_KEY_OEFF_HOEHE, "{:.4f}".format(float(oeff_hoehe))) + if oeff_brueest is not None: + obj_attrs.SetUserString(_KEY_OEFF_BRUEST, "{:.4f}".format(float(oeff_brueest))) + if oeff_rahmen_b is not None: + obj_attrs.SetUserString(_KEY_OEFF_RAHMEN_B, "{:.4f}".format(float(oeff_rahmen_b))) + if oeff_rahmen_tiefe is not None: + obj_attrs.SetUserString(_KEY_OEFF_RAHMEN_TIEFE, "{:.4f}".format(float(oeff_rahmen_tiefe))) + if oeff_rahmen_pos is not None and oeff_rahmen_pos in _OEFF_RAHMEN_POS_OPTIONS: + obj_attrs.SetUserString(_KEY_OEFF_RAHMEN_POS, oeff_rahmen_pos) + if oeff_fluegel is not None: + obj_attrs.SetUserString(_KEY_OEFF_FLUEGEL, "{}".format(int(oeff_fluegel))) + if oeff_sims_aus is not None: + # akzeptiere Bool (legacy) oder Style-String + if isinstance(oeff_sims_aus, bool): + v = "standard" if oeff_sims_aus else "ohne" + else: + v = str(oeff_sims_aus) + if v not in _OEFF_SIMS_STYLES: v = "ohne" + obj_attrs.SetUserString(_KEY_OEFF_SIMS_AUS, v) + if oeff_sims_in is not None: + if isinstance(oeff_sims_in, bool): + v = "standard" if oeff_sims_in else "ohne" + else: + v = str(oeff_sims_in) + if v not in _OEFF_SIMS_STYLES: v = "ohne" + obj_attrs.SetUserString(_KEY_OEFF_SIMS_IN, v) + if oeff_glas is not None: + obj_attrs.SetUserString(_KEY_OEFF_GLAS, "1" if bool(oeff_glas) else "0") + if oeff_referenz is not None and oeff_referenz in _OEFF_REFERENZ_OPTIONS: + obj_attrs.SetUserString(_KEY_OEFF_REFERENZ, oeff_referenz) + # --- Treppen-Felder --- + if geschoss_end is not None: + obj_attrs.SetUserString(_KEY_GESCHOSS_END, geschoss_end or "") + if treppe_breite is not None: + obj_attrs.SetUserString(_KEY_TREPPE_BREITE, "{:.4f}".format(float(treppe_breite))) + if treppe_n is not None: + obj_attrs.SetUserString(_KEY_TREPPE_N, "{}".format(int(treppe_n))) + if treppe_referenz is not None and treppe_referenz in ("mid", "links", "rechts"): + obj_attrs.SetUserString(_KEY_TREPPE_REFERENZ, treppe_referenz) + if treppe_modus is not None and treppe_modus in _TREPPE_MODI: + obj_attrs.SetUserString(_KEY_TREPPE_MODUS, treppe_modus) + if treppe_lauf_d is not None: + obj_attrs.SetUserString(_KEY_TREPPE_LAUF_D, "{:.4f}".format(float(treppe_lauf_d))) + if treppe_art is not None and treppe_art in _TREPPE_ARTEN: + obj_attrs.SetUserString(_KEY_TREPPE_ART, treppe_art) + if treppe_h_over is not None: + if treppe_h_over == "" or treppe_h_over is None: + obj_attrs.SetUserString(_KEY_TREPPE_H_OVER, "") + else: + try: + obj_attrs.SetUserString(_KEY_TREPPE_H_OVER, + "{:.4f}".format(float(treppe_h_over))) + except Exception: pass + if treppe_soll is not None: + try: + import json + obj_attrs.SetUserString(_KEY_TREPPE_SOLL, json.dumps(treppe_soll)) + except Exception: pass + + +def _read_meta(obj): + """Liest Element-Metadaten von einem Rhino-Objekt. Liefert dict oder None.""" + try: + a = obj.Attributes + type_ = a.GetUserString(_KEY_TYPE) + if not type_: return None + ref = a.GetUserString(_KEY_REFERENZ) or "mid" + if ref not in ("mid", "left", "right"): ref = "mid" + try: neigung = float(a.GetUserString(_KEY_DACH_NEIGUNG) or "30") + except Exception: neigung = 30.0 + try: eave = int(a.GetUserString(_KEY_DACH_EAVE) or "0") + except Exception: eave = 0 + dt = a.GetUserString(_KEY_DACH_TYP) or "pult" + if dt not in ("pult", "sattel", "walm", "mansarde"): dt = "pult" + try: nu = float(a.GetUserString(_KEY_DACH_NEIG_UNTEN) or "60") + except Exception: nu = 60.0 + try: kh = float(a.GetUserString(_KEY_DACH_KNICK_H) or "2.0") + except Exception: kh = 2.0 + dv = a.GetUserString(_KEY_DACH_VARIANTE) or "walm" + if dv not in ("walm", "giebel", "walm_giebel"): dv = "walm" + ot = a.GetUserString(_KEY_OEFF_TYP) or "" + if ot not in ("fenster", "tuer"): ot = "" + try: ob = float(a.GetUserString(_KEY_OEFF_BREITE) or "1.0") + except Exception: ob = 1.0 + try: oh = float(a.GetUserString(_KEY_OEFF_HOEHE) or "1.4") + except Exception: oh = 1.4 + try: obr = float(a.GetUserString(_KEY_OEFF_BRUEST) or "0.9") + except Exception: obr = 0.9 + try: or_b = float(a.GetUserString(_KEY_OEFF_RAHMEN_B) or "0.06") + except Exception: or_b = 0.06 + try: or_t = float(a.GetUserString(_KEY_OEFF_RAHMEN_TIEFE) or "0.08") + except Exception: or_t = 0.08 + or_p = a.GetUserString(_KEY_OEFF_RAHMEN_POS) or "mid" + if or_p not in _OEFF_RAHMEN_POS_OPTIONS: or_p = "mid" + try: ofl = int(a.GetUserString(_KEY_OEFF_FLUEGEL) or "1") + except Exception: ofl = 1 + if ofl < 1: ofl = 1 + # Sims-Stile + Glas-Default + is_fenster = (ot == "fenster") + def _sims_style(raw, default_fenster): + if raw in _OEFF_SIMS_STYLES: return raw + if raw == "1": return "standard" # Legacy bool true + if raw == "0": return "ohne" # Legacy bool false + return ("standard" if is_fenster else "ohne") if default_fenster else "ohne" + sa_raw = a.GetUserString(_KEY_OEFF_SIMS_AUS) or "" + si_raw = a.GetUserString(_KEY_OEFF_SIMS_IN) or "" + osa = _sims_style(sa_raw, True) + osi = _sims_style(si_raw, True) + og_raw = a.GetUserString(_KEY_OEFF_GLAS) + ogl = (og_raw == "1") if og_raw in ("0", "1") else is_fenster + oref = a.GetUserString(_KEY_OEFF_REFERENZ) or "mid" + if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid" + # Treppen-Felder + gend = a.GetUserString(_KEY_GESCHOSS_END) or "" + try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0") + except Exception: tb = 1.0 + try: tn = int(a.GetUserString(_KEY_TREPPE_N) or "15") + except Exception: tn = 15 + if tn < 2: tn = 2 + tref = a.GetUserString(_KEY_TREPPE_REFERENZ) or "mid" + if tref not in ("mid", "links", "rechts"): tref = "mid" + tmod = a.GetUserString(_KEY_TREPPE_MODUS) or "flach" + if tmod not in _TREPPE_MODI: tmod = "flach" + try: tld = float(a.GetUserString(_KEY_TREPPE_LAUF_D) or "0.18") + except Exception: tld = 0.18 + tart = a.GetUserString(_KEY_TREPPE_ART) or "gerade" + if tart not in _TREPPE_ARTEN: tart = "gerade" + thov = a.GetUserString(_KEY_TREPPE_H_OVER) or "" + # Soll-Werte JSON, mit Defaults wenn nicht gesetzt + import json + tsoll = dict(_TREPPE_SOLL_DEFAULT) + soll_raw = a.GetUserString(_KEY_TREPPE_SOLL) + if soll_raw: + try: + parsed = json.loads(soll_raw) + if isinstance(parsed, dict): + for k in ("s", "a", "sa"): + if k in parsed and isinstance(parsed[k], list) and len(parsed[k]) >= 3: + tsoll[k] = [float(parsed[k][0]), float(parsed[k][1]), + bool(parsed[k][2])] + except Exception: pass + return { + "id": a.GetUserString(_KEY_ID) or "", + "type": type_, + "geschoss": a.GetUserString(_KEY_GESCHOSS) or "", + "dicke": float(a.GetUserString(_KEY_DICKE) or "0.25"), + "uk_override": a.GetUserString(_KEY_UK_OVER) or "", + "ok_override": a.GetUserString(_KEY_OK_OVER) or "", + "referenz": ref, + "neigung": neigung, + "eave_idx": eave, + "dach_typ": dt, + "neigung_unten": nu, + "knick_h": kh, + "dach_variante": dv, + "oeff_typ": ot, + "oeff_parent": a.GetUserString(_KEY_OEFF_PARENT) or "", + "oeff_breite": ob, + "oeff_hoehe": oh, + "oeff_brueest": obr, + "oeff_rahmen_b": or_b, + "oeff_rahmen_tiefe": or_t, + "oeff_rahmen_pos": or_p, + "oeff_fluegel": ofl, + "oeff_sims_aus": osa, + "oeff_sims_in": osi, + "oeff_glas": ogl, + "oeff_referenz": oref, + "geschoss_end": gend, + "treppe_breite": tb, + "treppe_n": tn, + "treppe_referenz": tref, + "treppe_modus": tmod, + "treppe_lauf_d": tld, + "treppe_art": tart, + "treppe_h_over": thov, + "treppe_soll": tsoll, + } + except Exception: + return None + + +def _find_objects_by_wall_id(doc, wall_id, type_filter=None): + """Findet alle Rhino-Objekte mit der gegebenen wall_id.""" + out = [] + for obj in doc.Objects: + meta = _read_meta(obj) + if meta and meta["id"] == wall_id: + if type_filter is None or meta["type"] == type_filter: + out.append((obj, meta)) + return out + + +def _find_axis(doc, wall_id): + for obj, meta in _find_objects_by_wall_id(doc, wall_id, "wand_axis"): + return obj + return None + + +def _find_volume(doc, wall_id): + for obj, meta in _find_objects_by_wall_id(doc, wall_id, "wand_volume"): + return obj + return None + + +def _find_openings_for_wall(doc, wall_id): + """Alle Oeffnungs-Points (oeffnung_point) deren oeff_parent == wall_id.""" + out = [] + for obj in doc.Objects: + meta = _read_meta(obj) + if meta is None: continue + if meta["type"] != "oeffnung_point": continue + if meta.get("oeff_parent") != wall_id: continue + out.append((obj, meta)) + return out + + +def _oeff_effective_axis_point(axis_curve, point_on_axis, breite, referenz): + """Berechnet den effektiven Zentrums-Punkt der Oeffnung auf der Wand- + Achse — abhaengig davon ob der Klick-Punkt am linken/rechten Rand + oder in der Mitte der Oeffnung liegen soll. + + referenz="mid" → Punkt liegt mittig: Zentrum = Klick-Punkt + referenz="links" → Klick-Punkt am linken Rand: Zentrum = pt + tan*half + referenz="rechts" → Klick-Punkt am rechten Rand: Zentrum = pt - tan*half + + "links"/"rechts" beziehen sich auf die +tan/-tan Richtung der Wand- + Achse am Klick-Punkt. Mathematisch geht der Walk entlang der echten + Bogenlaenge auf der Kurve — funktioniert auch fuer gebogene Achsen.""" + if referenz not in ("links", "rechts"): + return point_on_axis + if not isinstance(axis_curve, rg.Curve): + return point_on_axis + half = float(breite) * 0.5 + try: + ok, t = axis_curve.ClosestPoint(point_on_axis) + if not ok: return point_on_axis + # Aktuelle Bogenlaenge vom Kurvenanfang bis t + sub = rg.Interval(axis_curve.Domain.Min, t) + try: arc_cur = axis_curve.GetLength(sub) + except Exception: + dom = axis_curve.Domain + arc_cur = ((t - dom.Min) / dom.Length) * axis_curve.GetLength() + if referenz == "links": + arc_new = arc_cur + half # Zentrum +tan/2 vom Klick + else: # rechts + arc_new = arc_cur - half # Zentrum -tan/2 + total = axis_curve.GetLength() + if arc_new < 0: arc_new = 0 + if arc_new > total: arc_new = total + lp = axis_curve.LengthParameter(arc_new) + t_new = None + if isinstance(lp, tuple) and len(lp) >= 2 and lp[0]: + t_new = lp[1] + if t_new is None: + # Fallback: lineare Parameter-Interpolation + dom = axis_curve.Domain + t_new = dom.Min + (arc_new / total) * dom.Length + return axis_curve.PointAt(t_new) + except Exception: + return point_on_axis + + +def _make_oeffnung_cutout(axis_curve, point_on_axis, wall_dicke, breite, + hoehe, brueest_h, base_z): + """Baut eine Cutout-Box die bei einer Wand-Regen via Boolean-Difference + abgezogen wird. Box ist zentriert am Point, entlang der Wand-Tangente + ausgerichtet, in Z von base_z+brueest bis base_z+brueest+hoehe, und + in Wand-Querrichtung leicht ueberdimensioniert (1.5x dicke) damit der + Schnitt sauber durch die Wand geht.""" + if not isinstance(axis_curve, rg.Curve): return None + try: + ok, t = axis_curve.ClosestPoint(point_on_axis) + if not ok: return None + pt = axis_curve.PointAt(t) + tan = axis_curve.TangentAt(t) + tan = rg.Vector3d(tan.X, tan.Y, 0) + if tan.Length < 1e-9: return None + tan.Unitize() + perp = rg.Vector3d(-tan.Y, tan.X, 0) + + half_b = float(breite) * 0.5 + half_d = float(wall_dicke) * 1.5 # ueberdimensioniert quer zur Wand + z_low = float(base_z) + float(brueest_h) + z_high = z_low + float(hoehe) + if z_high <= z_low + 1e-9: return None + + c0 = rg.Point3d(pt.X - tan.X * half_b - perp.X * half_d, + pt.Y - tan.Y * half_b - perp.Y * half_d, z_low) + c1 = rg.Point3d(pt.X + tan.X * half_b - perp.X * half_d, + pt.Y + tan.Y * half_b - perp.Y * half_d, z_low) + c2 = rg.Point3d(pt.X + tan.X * half_b + perp.X * half_d, + pt.Y + tan.Y * half_b + perp.Y * half_d, z_low) + c3 = rg.Point3d(pt.X - tan.X * half_b + perp.X * half_d, + pt.Y - tan.Y * half_b + perp.Y * half_d, z_low) + + poly = rg.Polyline([c0, c1, c2, c3, c0]) + base_curve = rg.PolylineCurve(poly) + extrusion = rg.Extrusion.Create(base_curve, z_high - z_low, True) + if extrusion is None: return None + return extrusion.ToBrep() + except Exception as ex: + print("[ELEMENTE] _make_oeffnung_cutout:", ex) + return None + + +def _oeff_axis_frame(axis_curve, point_on_axis): + """Liefert (pt, tan, perp) — pt projiziert auf Achse, tan = Wandrichtung + (XY-Projektion), perp = 90° CCW von tan (in XY). Return None bei Fehler.""" + if not isinstance(axis_curve, rg.Curve): return None + try: + ok, t = axis_curve.ClosestPoint(point_on_axis) + if not ok: return None + pt = axis_curve.PointAt(t) + tan = axis_curve.TangentAt(t) + tan = rg.Vector3d(tan.X, tan.Y, 0) + if tan.Length < 1e-9: return None + tan.Unitize() + perp = rg.Vector3d(-tan.Y, tan.X, 0) + return pt, tan, perp + except Exception: + return None + + +def _make_oeff_box(pt, tan, tan_lo, tan_hi, z_lo, z_hi, perp_lo, perp_hi): + """Baut eine achsen-orientierte Box-Brep im lokalen tan/Z/perp-System + relativ zur Wand-Achse am Punkt `pt`. Liefert Brep oder None.""" + try: + plane = rg.Plane(rg.Point3d(pt.X, pt.Y, 0), tan, + rg.Vector3d(0, 0, 1)) + if tan_hi <= tan_lo + 1e-9 or z_hi <= z_lo + 1e-9 or perp_hi <= perp_lo + 1e-9: + return None + box = rg.Box(plane, + rg.Interval(tan_lo, tan_hi), + rg.Interval(z_lo, z_hi), + rg.Interval(perp_lo, perp_hi)) + return box.ToBrep() + except Exception as ex: + print("[ELEMENTE] _make_oeff_box:", ex) + return None + + +def _resolve_rahmen_perp_range(half_d, rahmen_tiefe, rahmen_pos): + """Berechnet (perp_lo, perp_hi) entlang Plane.ZAxis fuer den Rahmen + je nach Position-Praeset. half_d = halbe Wandtiefe, rahmen_tiefe = + Profil-Tiefe entlang Wandnormale. + + Konvention: Plane.ZAxis fuer unsere Box-Konstruktion ist + tan x (0,0,1) — d.h. eine Seite der Wand. "aussen" mappt empirisch + auf +Plane.ZAxis-Seite (kann je nach Wand-Achsrichtung andersrum + sein — User kann Wand-Achse mit Rhino-Befehl Dir umdrehen).""" + rt = max(0.01, float(rahmen_tiefe)) + # Wenn Tiefe groesser als Wand → klammern (Inset 1mm damit kein Z-Fight) + rt = min(rt, 2.0 * half_d - 0.002) + if rt <= 0: rt = max(0.01, 2.0 * half_d - 0.002) + inset = 0.001 + if rahmen_pos == "aussen": + hi = +half_d - inset + lo = hi - rt + elif rahmen_pos == "innen": + lo = -half_d + inset + hi = lo + rt + else: # mid + lo = -rt * 0.5 + hi = +rt * 0.5 + return lo, hi + + +def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base_z): + """Baut die einzelnen Brep-Pieces der Oeffnung — Rahmen (single Brep + via Boolean-Differenz), Mittelpfosten (pro Fluegel), Glas, Sims aussen, + Sims innen. Liefert eine Liste von Breps. Caller persistiert jedes + als eigenes 'oeffnung_volume' Object mit der gleichen Oeffnungs-ID.""" + frame = _oeff_axis_frame(axis_curve, point_on_axis) + if frame is None: return [] + pt, tan, perp = frame + + breite = float(oeff_meta.get("oeff_breite", 1.0)) + hoehe = float(oeff_meta.get("oeff_hoehe", 1.4)) + brueest = float(oeff_meta.get("oeff_brueest", 0.9)) + rahmen_b = float(oeff_meta.get("oeff_rahmen_b", 0.06)) + rahmen_t = float(oeff_meta.get("oeff_rahmen_tiefe", 0.08)) + rahmen_pos = oeff_meta.get("oeff_rahmen_pos", "mid") + fluegel = max(1, int(oeff_meta.get("oeff_fluegel", 1))) + sims_aus_style = oeff_meta.get("oeff_sims_aus", "ohne") + sims_in_style = oeff_meta.get("oeff_sims_in", "ohne") + has_glas = bool(oeff_meta.get("oeff_glas", False)) + is_tuer = (oeff_meta.get("oeff_typ") == "tuer") + + half_b = breite * 0.5 + half_d = float(wall_dicke) * 0.5 + z_lo = float(base_z) + brueest + z_hi = z_lo + hoehe + + inner_l = -half_b + rahmen_b + inner_r = +half_b - rahmen_b + # Bei Tueren: KEIN unterer Riegel (Zarge ist 3-seitig). Das Innen- + # Loch geht bis unter den Outer-Box-Boden, sodass der Boolean-Diff + # den unteren Riegel wegschneidet. + inner_z_lo_frame = (z_lo - 0.01) if is_tuer else (z_lo + rahmen_b) + inner_z_hi_frame = z_hi - rahmen_b + # Fuer Mittelpfosten/Glas: bei Tueren beginnen die bei z_lo (Boden), + # bei Fenstern oberhalb des unteren Rahmens. + payload_z_lo = z_lo if is_tuer else (z_lo + rahmen_b) + payload_z_hi = z_hi - rahmen_b + if inner_l >= inner_r - 1e-6 or payload_z_lo >= payload_z_hi - 1e-6: + return [] # Rahmen-Profil zu dick fuer Oeffnung + + frame_perp_lo, frame_perp_hi = _resolve_rahmen_perp_range( + half_d, rahmen_t, rahmen_pos) + + pieces = [] + + # --- RAHMEN: outer box - inner box, sauberer single-Brep + try: + outer_box = _make_oeff_box(pt, tan, -half_b, +half_b, z_lo, z_hi, + frame_perp_lo, frame_perp_hi) + # Inner box leicht laenger in perp Richtung damit der Diff sauber + # durchschneidet (keine Hauchschicht uebrig). + inner_box = _make_oeff_box(pt, tan, inner_l, inner_r, + inner_z_lo_frame, inner_z_hi_frame, + frame_perp_lo - 0.01, frame_perp_hi + 0.01) + if outer_box is not None and inner_box is not None: + diff = rg.Brep.CreateBooleanDifference( + [outer_box], [inner_box], 0.001) + if diff and len(diff) > 0: + pieces.append(diff[0]) + else: + pieces.append(outer_box) + except Exception as ex: + print("[ELEMENTE] Rahmen BoolDiff:", ex) + + # --- Mittelpfosten (Fluegel > 1): kleine Stege im inneren Bereich + if fluegel > 1: + span = inner_r - inner_l + for i in range(1, fluegel): + x_mid = inner_l + span * (float(i) / fluegel) + x_lo = x_mid - rahmen_b * 0.5 + x_hi = x_mid + rahmen_b * 0.5 + mp = _make_oeff_box(pt, tan, x_lo, x_hi, payload_z_lo, payload_z_hi, + frame_perp_lo, frame_perp_hi) + if mp is not None: pieces.append(mp) + + # --- INNERE FUELLUNG: Glas (Fenster oder verglaste Tuer) ODER + # Tuerblatt (massive Tuere ohne Glas). Beides als Box-Brep pro Fluegel. + if is_tuer and not has_glas: + # Tuerblatt — 40 mm massive Platte, mittig in Rahmen-Tiefe + fill_t = 0.04 + elif has_glas: + fill_t = 0.012 # 12 mm Glas + else: + fill_t = 0 # nichts (z.B. Fenster ohne Glas) + + if fill_t > 0: + fill_mid = (frame_perp_lo + frame_perp_hi) * 0.5 + fill_lo = fill_mid - fill_t * 0.5 + fill_hi = fill_mid + fill_t * 0.5 + if fluegel > 1: + span = inner_r - inner_l + for i in range(fluegel): + fx_lo = inner_l + span * (float(i) / fluegel) + fx_hi = inner_l + span * (float(i + 1) / fluegel) + if i > 0: fx_lo += rahmen_b * 0.5 + if i < fluegel - 1: fx_hi -= rahmen_b * 0.5 + fp = _make_oeff_box(pt, tan, fx_lo, fx_hi, + payload_z_lo, payload_z_hi, + fill_lo, fill_hi) + if fp is not None: pieces.append(fp) + else: + fp = _make_oeff_box(pt, tan, inner_l, inner_r, + payload_z_lo, payload_z_hi, + fill_lo, fill_hi) + if fp is not None: pieces.append(fp) + + # --- SIMS AUSSEN (+Plane.ZAxis-Seite) — Platte unter der Oeffnung + sa = _OEFF_SIMS_STYLES.get(sims_aus_style) + if sa is not None: + s_t = sa["dicke"]; s_pr = sa["aus"]; s_oh = sa["ueberhang"] + s_lo = z_lo - s_t + sb = _make_oeff_box(pt, tan, + -half_b - s_oh, +half_b + s_oh, + s_lo, z_lo, + +half_d, +half_d + s_pr) + if sb is not None: pieces.append(sb) + + # --- SIMS INNEN (-Plane.ZAxis-Seite) — Platte unter der Oeffnung + si = _OEFF_SIMS_STYLES.get(sims_in_style) + if si is not None: + s_t = si["dicke"]; s_pr = si["aus"]; s_oh = si["ueberhang"] + s_lo = z_lo - s_t + sb = _make_oeff_box(pt, tan, + -half_b - s_oh, +half_b + s_oh, + s_lo, z_lo, + -half_d - s_pr, -half_d) + if sb is not None: pieces.append(sb) + + return pieces + + +SOURCE_TYPES = ("wand_axis", "decke_outline", "dach_outline", + "oeffnung_point", "treppe_axis") +VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume", + "oeffnung_volume", "treppe_volume") +# Oeffnungs-Cutout: Boolean-Difference aus Wand. Zusaetzlich kriegt die +# Oeffnung ihr eigenes Volumen (Rahmen + Sims + Glas) als Sub-Element. + + +def _find_source(doc, element_id): + """Source-Objekt — Achse (Wand) bzw. Outline (Decke/Dach).""" + for obj, meta in _find_objects_by_wall_id(doc, element_id): + if meta["type"] in SOURCE_TYPES: + return obj, meta + return None, None + + +def _find_target_volume(doc, element_id): + """Volumen-Objekt (Brep).""" + for obj, meta in _find_objects_by_wall_id(doc, element_id): + if meta["type"] in VOLUME_TYPES: + return obj + return None + + +# --- Dach-Helpers (Pultdach) ------------------------------------------------- + +def _resolve_dach_base(doc, gid, uk_over): + """Basis-Hoehe des Dachs an der Traufe (= Eave) = OKFF + Hoehe des + Geschosses (Oberkante der Waende). uk_override kann das ueberschreiben.""" + g = _geschoss_by_id(doc, gid) + if g is None: + return float(uk_over) if uk_over not in (None, "") else 3.0 + okff = float(g.get("okff", 0.0)) + hoehe = float(g.get("hoehe", 3.0)) + auto = okff + hoehe + return float(uk_over) if uk_over not in (None, "") else auto + + +def _outline_points_xy(outline_curve): + """Extrahiert XY-Vertices einer geschlossenen Polyline (ohne Schluss- + Duplikat). Liefert Liste von Point3d mit Z=0.""" + if not isinstance(outline_curve, rg.Curve): return [] + ok, poly = outline_curve.TryGetPolyline() + if not ok or poly is None: return [] + pts = [poly[i] for i in range(poly.Count)] + if len(pts) > 1 and pts[0].DistanceTo(pts[-1]) < 1e-6: + pts = pts[:-1] + return [rg.Point3d(p.X, p.Y, 0) for p in pts] + + +def _thicken_roof_inward(top_brep, dicke, tol=0.001): + """Verdickt eine offene Brep-Schale (oberes Dachshell) entlang der + Flaechen-Normalen um `dicke` nach innen — d.h. senkrecht zur jeweiligen + Dachflaeche, nicht nur vertikal. Liefert geschlossenen Festkoerper-Brep. + + Die Normalen-Richtung der gejointen Schale haengt von der Zeichen- + richtung der Outline ab (CW vs CCW von oben). Statt blind ein + Vorzeichen zu raten probiert die Funktion beide Richtungen und + waehlt das Resultat das tatsaechlich nach UNTEN extrudiert (d.h. + die Unterseite des Daches unter der originalen Aussenflaeche).""" + if top_brep is None: return None + d = float(dicke) + if d <= 1e-9: return top_brep + + orig_bbox = top_brep.GetBoundingBox(True) + orig_min_z = orig_bbox.Min.Z + + def _try(distance, extend): + try: + result = rg.Brep.CreateOffsetBrep(top_brep, distance, True, extend, tol) + if isinstance(result, tuple): + arr = result[0] + elif hasattr(result, '__len__'): + arr = result + else: + arr = None + if arr and len(arr) > 0: + return arr[0] + except Exception as ex: + print("[ELEMENTE] CreateOffsetBrep ({}, extend={}):".format(distance, extend), ex) + return None + + # Probiere beide Vorzeichen (+/-d), beide extend-Varianten. + candidates = [] + for distance in (-d, d): + r = _try(distance, False) + if r is None: + r = _try(distance, True) + if r is not None: + candidates.append(r) + + if not candidates: return None + + # Bevorzuge das Resultat das nach UNTEN extrudiert — die Unterseite des + # Daches muss unter dem original Top-Brep-Min-Z liegen. Sonst geht die + # Verdickung nach aussen (nach oben), was wir nicht wollen. + threshold = max(1e-4, d * 0.05) + for c in candidates: + bb = c.GetBoundingBox(True) + if bb.Min.Z < orig_min_z - threshold: + return c + return candidates[0] + + +def _join_open_shell(faces, tol=0.001): + """Joined eine Liste planarer Brep-Faces zu einer offenen Brep-Schale.""" + valid = [f for f in faces if f is not None] + if not valid: return None + try: + joined = rg.Brep.JoinBreps(valid, tol) + if joined and len(joined) > 0: + return joined[0] + except Exception as ex: + print("[ELEMENTE] _join_open_shell:", ex) + return None + + +def _make_pultdach_volume(outline_curve, dicke, base_height, slope_deg, eave_idx): + """Pultdach: obere Dachflaeche liegt geneigt auf der Eckpunkt-Hoehe + je Punkt (Eave bei base_height, opposite Seite ansteigend). Die + Dicke wird senkrecht zur Dachflaeche nach innen extrudiert (via + CreateOffsetBrep). Liefert geschlossenen Festkoerper-Brep oder None.""" + import math + pts_xy = _outline_points_xy(outline_curve) + n = len(pts_xy) + if n < 3: return None + if eave_idx < 0 or eave_idx >= n: eave_idx = 0 + a = pts_xy[eave_idx] + b = pts_xy[(eave_idx + 1) % n] + eave_vec = rg.Vector3d(b.X - a.X, b.Y - a.Y, 0) + if eave_vec.Length < 1e-9: return None + eave_vec.Unitize() + perp = rg.Vector3d(-eave_vec.Y, eave_vec.X, 0) + sample_idx = (eave_idx + 2) % n + sv = rg.Vector3d(pts_xy[sample_idx].X - a.X, pts_xy[sample_idx].Y - a.Y, 0) + d_sample = sv.X * perp.X + sv.Y * perp.Y + if d_sample < 0: + perp = -perp + + tan_s = math.tan(math.radians(float(slope_deg))) + top_pts = [] + for p in pts_xy: + dv = rg.Vector3d(p.X - a.X, p.Y - a.Y, 0) + d = dv.X * perp.X + dv.Y * perp.Y + if d < 0: d = 0.0 + z_top = base_height + d * tan_s + top_pts.append(rg.Point3d(p.X, p.Y, z_top)) + top_pts.append(top_pts[0]) + + tol = 0.001 + try: + top_curve = rg.PolylineCurve(rg.Polyline(top_pts)) + top_faces = rg.Brep.CreatePlanarBreps([top_curve], tol) + if not top_faces or len(top_faces) == 0: return None + top_brep = top_faces[0] + return _thicken_roof_inward(top_brep, dicke, tol) + except Exception as ex: + print("[ELEMENTE] Pultdach Brep:", ex) + return None + + +def _make_satteldach_brep(outline_curve, dicke, base_height, slope_deg, ridge_along='long'): + """Satteldach: zwei geneigte Trapeze treffen sich am First, mit + senkrechter Dicke nach innen extrudiert via CreateOffsetBrep. + Erfordert eine 4-Punkt-Outline (Rechteck). ridge_along='long' → + First entlang der laengeren Achse, 'short' → entlang der kuerzeren.""" + import math + pts = _outline_points_xy(outline_curve) + if len(pts) != 4: return None + e01 = pts[0].DistanceTo(pts[1]) + e12 = pts[1].DistanceTo(pts[2]) + long_axis_is_01 = e01 >= e12 + use_01_as_long = long_axis_is_01 if ridge_along == 'long' else (not long_axis_is_01) + short_len = min(e01, e12) if use_01_as_long else max(e01, e12) + # short_len = die Spannweite quer zur First-Achse + if use_01_as_long: + span = e12 # quer zur First-Achse (= zu Edges 0-1, 2-3) + else: + span = e01 + half_span = span * 0.5 + ridge_z = base_height + half_span * math.tan(math.radians(float(slope_deg))) + + c0 = rg.Point3d(pts[0].X, pts[0].Y, base_height) + c1 = rg.Point3d(pts[1].X, pts[1].Y, base_height) + c2 = rg.Point3d(pts[2].X, pts[2].Y, base_height) + c3 = rg.Point3d(pts[3].X, pts[3].Y, base_height) + if use_01_as_long: + # First parallel zu 0-1 und 2-3 → Endpunkte mid(1-2) und mid(3-0) + ridge_a = rg.Point3d((pts[3].X + pts[0].X) * 0.5, + (pts[3].Y + pts[0].Y) * 0.5, ridge_z) + ridge_b = rg.Point3d((pts[1].X + pts[2].X) * 0.5, + (pts[1].Y + pts[2].Y) * 0.5, ridge_z) + # Trapeze (outer-facing, CCW von aussen) + face_a_pts = [c0, c1, ridge_b, ridge_a, c0] # Seite 0-1 + face_b_pts = [c2, c3, ridge_a, ridge_b, c2] # Seite 2-3 + else: + ridge_a = rg.Point3d((pts[0].X + pts[1].X) * 0.5, + (pts[0].Y + pts[1].Y) * 0.5, ridge_z) + ridge_b = rg.Point3d((pts[2].X + pts[3].X) * 0.5, + (pts[2].Y + pts[3].Y) * 0.5, ridge_z) + face_a_pts = [c1, c2, ridge_b, ridge_a, c1] # Seite 1-2 + face_b_pts = [c3, c0, ridge_a, ridge_b, c3] # Seite 3-0 + + tol = 0.001 + faces = [_planar_face_from_pts(face_a_pts, tol), + _planar_face_from_pts(face_b_pts, tol)] + top_brep = _join_open_shell(faces, tol) + return _thicken_roof_inward(top_brep, dicke, tol) + + +def _make_walmdach_brep(outline_curve, dicke, base_height, slope_deg): + """Walmdach fuer Rechteck-Outline: 4 geneigte Flaechen, die am First + zusammenlaufen. Bei einem Quadrat → Zeltdach (= Pyramidendach).""" + import math + pts = _outline_points_xy(outline_curve) + if len(pts) != 4: return None + e01 = pts[0].DistanceTo(pts[1]) + e12 = pts[1].DistanceTo(pts[2]) + long_axis_is_01 = e01 >= e12 + # First entlang langer Achse; Laenge des Firsts = laengere Seite minus + # kuerzere Seite (= 2x hip-inset) + long_len = max(e01, e12) + short_len = min(e01, e12) + half_short = short_len * 0.5 + tan_s = math.tan(math.radians(float(slope_deg))) + ridge_height = half_short * tan_s + # Firstpunkte = mittlere Punkte der kurzen Kanten, jeweils nach innen + # verschoben um half_short (= hip-inset, damit alle 4 Walmflaechen + # denselben Neigungswinkel haben) + if long_axis_is_01: + # Lange Kanten: 0-1 und 2-3. Kurze Kanten: 1-2 und 3-0. + mid_12 = rg.Point3d((pts[1].X + pts[2].X) * 0.5, + (pts[1].Y + pts[2].Y) * 0.5, 0) + mid_30 = rg.Point3d((pts[3].X + pts[0].X) * 0.5, + (pts[3].Y + pts[0].Y) * 0.5, 0) + long_dir = pts[1] - pts[0] + long_dir.Z = 0 + long_unit = rg.Vector3d(long_dir); long_unit.Unitize() + # ridge points sind die mids nach innen entlang -long_unit / +long_unit + ridge_a = rg.Point3d(mid_30.X + long_unit.X * half_short, + mid_30.Y + long_unit.Y * half_short, + base_height + ridge_height) + ridge_b = rg.Point3d(mid_12.X - long_unit.X * half_short, + mid_12.Y - long_unit.Y * half_short, + base_height + ridge_height) + else: + mid_01 = rg.Point3d((pts[0].X + pts[1].X) * 0.5, + (pts[0].Y + pts[1].Y) * 0.5, 0) + mid_23 = rg.Point3d((pts[2].X + pts[3].X) * 0.5, + (pts[2].Y + pts[3].Y) * 0.5, 0) + long_dir = pts[2] - pts[1] + long_dir.Z = 0 + long_unit = rg.Vector3d(long_dir); long_unit.Unitize() + ridge_a = rg.Point3d(mid_01.X + long_unit.X * half_short, + mid_01.Y + long_unit.Y * half_short, + base_height + ridge_height) + ridge_b = rg.Point3d(mid_23.X - long_unit.X * half_short, + mid_23.Y - long_unit.Y * half_short, + base_height + ridge_height) + # Outer (oben sichtbar) Eckpunkte auf base_height + c0 = rg.Point3d(pts[0].X, pts[0].Y, base_height) + c1 = rg.Point3d(pts[1].X, pts[1].Y, base_height) + c2 = rg.Point3d(pts[2].X, pts[2].Y, base_height) + c3 = rg.Point3d(pts[3].X, pts[3].Y, base_height) + is_square = abs(long_len - short_len) < 1e-6 + tol = 0.001 + faces = [] + + def add_face(pts): + f = _planar_face_from_pts(pts, tol) + if f: faces.append(f) + + # NUR die Outer-Schraegflaechen — die Dicke wird per CreateOffsetBrep + # senkrecht zur jeweiligen Flaeche nach innen extrudiert. + if long_axis_is_01: + if is_square: + apex_o = ridge_a + add_face([c0, c1, apex_o, c0]) + add_face([c1, c2, apex_o, c1]) + add_face([c2, c3, apex_o, c2]) + add_face([c3, c0, apex_o, c3]) + else: + add_face([c0, c1, ridge_b, ridge_a, c0]) + add_face([c2, c3, ridge_a, ridge_b, c2]) + add_face([c1, c2, ridge_b, c1]) + add_face([c3, c0, ridge_a, c3]) + else: + if is_square: + apex_o = ridge_a + add_face([c0, c1, apex_o, c0]) + add_face([c1, c2, apex_o, c1]) + add_face([c2, c3, apex_o, c2]) + add_face([c3, c0, apex_o, c3]) + else: + add_face([c1, c2, ridge_b, ridge_a, c1]) + add_face([c3, c0, ridge_a, ridge_b, c3]) + add_face([c0, c1, ridge_a, c0]) + add_face([c2, c3, ridge_b, c2]) + + top_brep = _join_open_shell(faces, tol) + return _thicken_roof_inward(top_brep, dicke, tol) + + +def _make_mansardendach_brep(outline_curve, dicke, base_height, + slope_upper_deg, slope_lower_deg, knick_h, + variante="walm"): + """Dispatcher: 'walm' = 4-seitig mit Knick rundum (Mansarden-Walm). + 'giebel' = klassisches Mansardendach mit vertikalen Giebel-Pentagonen + an den Schmalseiten (DACH-Region-Standard). + 'walm_giebel' = unten Walm (Knick rundum), oben Giebel (First ueber + voller Laenge, vertikale Dreieck-Giebel an den Schmalseiten).""" + if variante == "giebel": + return _make_mansardendach_giebel(outline_curve, dicke, base_height, + slope_upper_deg, slope_lower_deg, knick_h) + if variante == "walm_giebel": + return _make_mansardendach_walm_giebel(outline_curve, dicke, base_height, + slope_upper_deg, slope_lower_deg, knick_h) + return _make_mansardendach_walm(outline_curve, dicke, base_height, + slope_upper_deg, slope_lower_deg, knick_h) + + +def _make_mansardendach_giebel(outline_curve, dicke, base_height, + slope_upper_deg, slope_lower_deg, knick_h): + """Klassisches Mansardendach: Knick nur auf den 2 langen Seiten, an den + Schmalseiten vertikale Giebelwand-Pentagone bis hoch zum First.""" + import math + pts = _outline_points_xy(outline_curve) + if len(pts) != 4: return None + e01 = pts[0].DistanceTo(pts[1]) + e12 = pts[1].DistanceTo(pts[2]) + # Corners so umsortieren dass corners[0]→corners[1] die erste lange Kante ist + if e01 >= e12: + corners = [pts[0], pts[1], pts[2], pts[3]] + short_len = e12 + else: + corners = [pts[1], pts[2], pts[3], pts[0]] + short_len = e01 + half_short = short_len * 0.5 + # Long-Edge-Richtung + Inward-Perpendicular (zeigt vom 1. langen Edge weg) + lv = rg.Vector3d(corners[1].X - corners[0].X, corners[1].Y - corners[0].Y, 0) + if lv.Length < 1e-9: return None + lv.Unitize() + perp = rg.Vector3d(-lv.Y, lv.X, 0) + mid_first = rg.Point3d((corners[0].X + corners[1].X) * 0.5, + (corners[0].Y + corners[1].Y) * 0.5, 0) + mid_opp = rg.Point3d((corners[2].X + corners[3].X) * 0.5, + (corners[2].Y + corners[3].Y) * 0.5, 0) + if (mid_opp.X - mid_first.X) * perp.X + (mid_opp.Y - mid_first.Y) * perp.Y < 0: + perp = -perp + + tan_lower = math.tan(math.radians(float(slope_lower_deg))) + tan_upper = math.tan(math.radians(float(slope_upper_deg))) + if tan_lower <= 1e-9: + return _make_walmdach_brep(outline_curve, dicke, base_height, slope_upper_deg) + knick_inset = float(knick_h) / tan_lower + if knick_inset >= half_short - 1e-6: + return _make_walmdach_brep(outline_curve, dicke, base_height, slope_lower_deg) + remaining = half_short - knick_inset + ridge_above_knick = remaining * tan_upper + total_h = float(knick_h) + ridge_above_knick + + # Eave-Ecken + c = [rg.Point3d(p.X, p.Y, base_height) for p in corners] + # Knick-Ecken: corners[0,1] auf 1. langer Kante → Knick in +perp; + # corners[2,3] auf gegenueberliegender Kante → Knick in -perp. + # WICHTIG: in der GIEBEL-Variante bleiben die Knick-Ecken auf demselben + # X (entlang langer Achse) wie die zugehoerige Eave-Ecke — KEIN diagonaler + # Inset wie bei Walm. So liegen sie in der Vertikal-Ebene der Gable. + k = [ + rg.Point3d(c[0].X + perp.X * knick_inset, c[0].Y + perp.Y * knick_inset, base_height + knick_h), + rg.Point3d(c[1].X + perp.X * knick_inset, c[1].Y + perp.Y * knick_inset, base_height + knick_h), + rg.Point3d(c[2].X - perp.X * knick_inset, c[2].Y - perp.Y * knick_inset, base_height + knick_h), + rg.Point3d(c[3].X - perp.X * knick_inset, c[3].Y - perp.Y * knick_inset, base_height + knick_h), + ] + # First-Endpunkte: Mittelpunkte der Gable-Kanten — diese liegen + # geometrisch BEREITS auf der Centerline der Langachse. Kein zusaetzlicher + # Inset noetig (der war im alten Code falsch — verschob den First auf + # einen Eckpunkt und stauchte die Gable-Pentagone). + ridge_w = rg.Point3d((c[3].X + c[0].X) * 0.5, (c[3].Y + c[0].Y) * 0.5, + base_height + total_h) + ridge_e = rg.Point3d((c[1].X + c[2].X) * 0.5, (c[1].Y + c[2].Y) * 0.5, + base_height + total_h) + + tol = 0.001 + faces = [] + def add(pl): + f = _planar_face_from_pts(pl, tol) + if f: faces.append(f) + + # NUR Outer-Schale (4 Dachflaechen + 2 vertikale Giebel-Pentagone). + # Dicke wird per CreateOffsetBrep senkrecht zur jeweiligen Flaeche + # nach innen extrudiert. + add([c[0], c[1], k[1], k[0], c[0]]) + add([c[2], c[3], k[3], k[2], c[2]]) + add([k[0], k[1], ridge_e, ridge_w, k[0]]) + add([k[2], k[3], ridge_w, ridge_e, k[2]]) + add([c[0], c[3], k[3], ridge_w, k[0], c[0]]) # West-Giebel + add([c[1], c[2], k[2], ridge_e, k[1], c[1]]) # Ost-Giebel + + top_brep = _join_open_shell(faces, tol) + return _thicken_roof_inward(top_brep, dicke, tol) + + +def _make_mansardendach_walm(outline_curve, dicke, base_height, + slope_upper_deg, slope_lower_deg, knick_h): + """Mansardendach (4-seitig, Walm-Mansarde): 4 Eaves mit Knick rundum, + unten steile Flaeche, oben flachere Walm-Kappe. Erfordert 4-Punkt-Outline.""" + import math + pts = _outline_points_xy(outline_curve) + if len(pts) != 3 + 1 and len(pts) != 4: return None + if len(pts) != 4: return None + + e01 = pts[0].DistanceTo(pts[1]) + e12 = pts[1].DistanceTo(pts[2]) + long_axis_is_01 = e01 >= e12 + short_len = min(e01, e12) + half_short = short_len * 0.5 + + tan_lower = math.tan(math.radians(float(slope_lower_deg))) + tan_upper = math.tan(math.radians(float(slope_upper_deg))) + knick_h = float(knick_h) + if tan_lower <= 1e-9: + return _make_walmdach_brep(outline_curve, dicke, base_height, slope_upper_deg) + knick_inset = knick_h / tan_lower + if knick_inset >= half_short - 1e-6: + # Knick zu hoch → degeneriert zu reinem Walm + return _make_walmdach_brep(outline_curve, dicke, base_height, slope_lower_deg) + remaining = half_short - knick_inset + ridge_above_knick = remaining * tan_upper + total_height = knick_h + ridge_above_knick + + # Eave-Ecken auf base_height + c0 = rg.Point3d(pts[0].X, pts[0].Y, base_height) + c1 = rg.Point3d(pts[1].X, pts[1].Y, base_height) + c2 = rg.Point3d(pts[2].X, pts[2].Y, base_height) + c3 = rg.Point3d(pts[3].X, pts[3].Y, base_height) + # Knick-Ecken: nach innen verschoben (Diagonal-Approximation fuer Rechteck) + cx = (pts[0].X + pts[1].X + pts[2].X + pts[3].X) * 0.25 + cy = (pts[0].Y + pts[1].Y + pts[2].Y + pts[3].Y) * 0.25 + def inset_corner(corner): + dx = cx - corner.X; dy = cy - corner.Y + L = (dx * dx + dy * dy) ** 0.5 + if L < 1e-9: + return rg.Point3d(corner.X, corner.Y, base_height + knick_h) + # Diagonale Verschiebung = knick_inset / cos(45°) ≈ knick_inset * sqrt(2) + # fuer ein achsenaligned Rechteck; sonst Approximation. + f = (knick_inset * 1.41421356) / L + return rg.Point3d(corner.X + dx * f, corner.Y + dy * f, + base_height + knick_h) + k0 = inset_corner(pts[0]) + k1 = inset_corner(pts[1]) + k2 = inset_corner(pts[2]) + k3 = inset_corner(pts[3]) + + # Ridge entlang langer Achse — MIT Hip-Inset, damit oben drauf ein + # echtes Walmdach steht (nicht ein Sattel mit degenerierten + # Walm-Dreiecken). Inset-Distanz = `remaining` = halbe kurze Seite + # des Knick-Polygons. So treffen sich die Walm-Hipflaechen unter dem + # gleichen Neigungswinkel wie die Trapezflaechen. + if long_axis_is_01: + long_dir = rg.Vector3d(pts[1].X - pts[0].X, pts[1].Y - pts[0].Y, 0) + else: + long_dir = rg.Vector3d(pts[2].X - pts[1].X, pts[2].Y - pts[1].Y, 0) + long_dir.Z = 0 + long_dir.Unitize() + if long_axis_is_01: + mid_west = rg.Point3d((k3.X + k0.X) * 0.5, (k3.Y + k0.Y) * 0.5, 0) + mid_east = rg.Point3d((k1.X + k2.X) * 0.5, (k1.Y + k2.Y) * 0.5, 0) + ra = rg.Point3d(mid_west.X + long_dir.X * remaining, + mid_west.Y + long_dir.Y * remaining, + base_height + total_height) + rb = rg.Point3d(mid_east.X - long_dir.X * remaining, + mid_east.Y - long_dir.Y * remaining, + base_height + total_height) + else: + mid_south = rg.Point3d((k0.X + k1.X) * 0.5, (k0.Y + k1.Y) * 0.5, 0) + mid_north = rg.Point3d((k2.X + k3.X) * 0.5, (k2.Y + k3.Y) * 0.5, 0) + ra = rg.Point3d(mid_south.X + long_dir.X * remaining, + mid_south.Y + long_dir.Y * remaining, + base_height + total_height) + rb = rg.Point3d(mid_north.X - long_dir.X * remaining, + mid_north.Y - long_dir.Y * remaining, + base_height + total_height) + is_square = abs(ra.DistanceTo(rb)) < 1e-6 # → Zelt-Mansarde + + tol = 0.001 + faces = [] + def add(pts_list): + f = _planar_face_from_pts(pts_list, tol) + if f: faces.append(f) + + # NUR Outer-Schale — Dicke wird via CreateOffsetBrep senkrecht zur + # jeweiligen Flaeche nach innen extrudiert. + # 1) Untere steile Mansarde-Flaechen (4) + add([c0, c1, k1, k0, c0]) + add([c1, c2, k2, k1, c1]) + add([c2, c3, k3, k2, c2]) + add([c3, c0, k0, k3, c3]) + + # 2) Obere flachere Walm-Kappe + if long_axis_is_01: + if is_square: + apex_o = ra + for tri in ((k0, k1, apex_o), (k1, k2, apex_o), + (k2, k3, apex_o), (k3, k0, apex_o)): + add([tri[0], tri[1], tri[2], tri[0]]) + else: + add([k0, k1, rb, ra, k0]) + add([k2, k3, ra, rb, k2]) + add([k1, k2, rb, k1]) + add([k3, k0, ra, k3]) + else: + if is_square: + apex_o = ra + for tri in ((k0, k1, apex_o), (k1, k2, apex_o), + (k2, k3, apex_o), (k3, k0, apex_o)): + add([tri[0], tri[1], tri[2], tri[0]]) + else: + add([k1, k2, rb, ra, k1]) + add([k3, k0, ra, rb, k3]) + add([k0, k1, ra, k0]) + add([k2, k3, rb, k2]) + + top_brep = _join_open_shell(faces, tol) + return _thicken_roof_inward(top_brep, dicke, tol) + + +def _make_mansardendach_walm_giebel(outline_curve, dicke, base_height, + slope_upper_deg, slope_lower_deg, knick_h): + """Mansardendach Walm-Giebel: unten Walm (Knick rundum, 4 steile + Mansarde-Flaechen), oben Giebel (First ueber voller Laenge, 2 obere + Dachflaechen entlang der langen Seiten, vertikale Dreieck-Giebel an + den Schmalseiten). Erfordert Rechteck-Outline.""" + import math + pts = _outline_points_xy(outline_curve) + if len(pts) != 4: return None + + e01 = pts[0].DistanceTo(pts[1]) + e12 = pts[1].DistanceTo(pts[2]) + long_axis_is_01 = e01 >= e12 + short_len = min(e01, e12) + half_short = short_len * 0.5 + + tan_lower = math.tan(math.radians(float(slope_lower_deg))) + tan_upper = math.tan(math.radians(float(slope_upper_deg))) + knick_h = float(knick_h) + if tan_lower <= 1e-9: + # Untere Mansarde degeneriert → reines Satteldach mit slope_upper + return _make_satteldach_brep(outline_curve, dicke, base_height, + slope_upper_deg) + knick_inset = knick_h / tan_lower + if knick_inset >= half_short - 1e-6: + # Knick zu hoch → unten ginge bis zur Spitze, oben kein Platz mehr + return _make_walmdach_brep(outline_curve, dicke, base_height, + slope_lower_deg) + remaining = half_short - knick_inset + ridge_above_knick = remaining * tan_upper + total_height = knick_h + ridge_above_knick + + # Eave-Ecken + c0 = rg.Point3d(pts[0].X, pts[0].Y, base_height) + c1 = rg.Point3d(pts[1].X, pts[1].Y, base_height) + c2 = rg.Point3d(pts[2].X, pts[2].Y, base_height) + c3 = rg.Point3d(pts[3].X, pts[3].Y, base_height) + cx = (pts[0].X + pts[1].X + pts[2].X + pts[3].X) * 0.25 + cy = (pts[0].Y + pts[1].Y + pts[2].Y + pts[3].Y) * 0.25 + def inset_corner(corner): + dx = cx - corner.X; dy = cy - corner.Y + L = (dx * dx + dy * dy) ** 0.5 + if L < 1e-9: + return rg.Point3d(corner.X, corner.Y, base_height + knick_h) + f = (knick_inset * 1.41421356) / L + return rg.Point3d(corner.X + dx * f, corner.Y + dy * f, + base_height + knick_h) + k0 = inset_corner(pts[0]) + k1 = inset_corner(pts[1]) + k2 = inset_corner(pts[2]) + k3 = inset_corner(pts[3]) + + # First: ueber voller Laenge des Knick-Polygons (KEIN Hip-Inset → Giebel oben) + if long_axis_is_01: + # Lange Seiten: k0-k1 und k2-k3 ; Schmalseiten: k1-k2 (Ost), k3-k0 (West) + ra = rg.Point3d((k3.X + k0.X) * 0.5, (k3.Y + k0.Y) * 0.5, + base_height + total_height) # West + rb = rg.Point3d((k1.X + k2.X) * 0.5, (k1.Y + k2.Y) * 0.5, + base_height + total_height) # Ost + else: + # Lange Seiten: k1-k2 und k3-k0 ; Schmalseiten: k0-k1 (Sued), k2-k3 (Nord) + ra = rg.Point3d((k0.X + k1.X) * 0.5, (k0.Y + k1.Y) * 0.5, + base_height + total_height) # Sued + rb = rg.Point3d((k2.X + k3.X) * 0.5, (k2.Y + k3.Y) * 0.5, + base_height + total_height) # Nord + + tol = 0.001 + faces = [] + def add(pts_list): + f = _planar_face_from_pts(pts_list, tol) + if f: faces.append(f) + + # NUR Outer-Schale — Dicke wird via CreateOffsetBrep senkrecht zur + # jeweiligen Flaeche nach innen extrudiert. + # 1) Untere steile Mansarde-Flaechen (4) + add([c0, c1, k1, k0, c0]) + add([c1, c2, k2, k1, c1]) + add([c2, c3, k3, k2, c2]) + add([c3, c0, k0, k3, c3]) + + # 2) Obere Giebel-Section: 2 Sattel-Flaechen + 2 vertikale Dreieck-Giebel + if long_axis_is_01: + add([k0, k1, rb, ra, k0]) + add([k2, k3, ra, rb, k2]) + add([k3, k0, ra, k3]) + add([k1, k2, rb, k1]) + else: + add([k1, k2, rb, ra, k1]) + add([k3, k0, ra, rb, k3]) + add([k0, k1, ra, k0]) + add([k2, k3, rb, k2]) + + top_brep = _join_open_shell(faces, tol) + return _thicken_roof_inward(top_brep, dicke, tol) + + +def _planar_face_from_pts(pts, tol): + """Erzeugt eine planare Brep-Flaeche aus einer Liste von Eckpunkten.""" + try: + curve = rg.PolylineCurve(rg.Polyline(pts)) + result = rg.Brep.CreatePlanarBreps([curve], tol) + if result and len(result) > 0: + return result[0] + except Exception: + pass + return None + + +# --- Decken-Volumen --------------------------------------------------------- + +def _make_decke_volume(outline_curve, dicke, uk, ok): + """Decke = Extrusion einer geschlossenen planaren Curve von UK bis OK.""" + if not isinstance(outline_curve, rg.Curve): return None + if not outline_curve.IsClosed: return None + height = float(ok) - float(uk) + if height <= 0: return None + profile = outline_curve.DuplicateCurve() + if abs(uk) > 1e-9: + profile.Transform(rg.Transform.Translation(0, 0, uk)) + extrusion = rg.Extrusion.Create(profile, height, True) + if extrusion is None: return None + return extrusion.ToBrep() + + +# --- Treppen-Volumen -------------------------------------------------------- + +def _treppe_profile_2d(N, S, A, uk, modus, lauf_d): + """Liefert das 2D-Seitenprofil der Treppe als Liste von (x, z)-Tupeln. + + Konvention: N Steigungen + N Auftritte (oberster Tritt gehoert zur + Treppe — fuegt sich sauber in die obere Decke ein, ohne freistehende + Setzstufe oben). Lauflaenge = N*A, Hoehe = N*S. + + Modi: + 'massiv' — Block bis zum Boden (uk) + 'flach' — schraege Plattenunterseite parallel zur Steigungslinie, + Plattendicke lauf_d vertikal gemessen + 'plattenrand' — Faltwerk-Treppe mit echten Vertikalen unter den + Setzstufen (Konvex-Ecken truncated, Konkav-Ecken haben + zusaetzliche L-Verlaengerung — Slab folgt dem oberen + Profil parallel) + """ + Z0 = float(uk) + d = float(lauf_d) + + # Oberes Profil: N Risers + N Treads (der letzte Tritt ist enthalten) + top = [(0.0, Z0)] + for k in range(N): + top.append((k * A, Z0 + (k + 1) * S)) # top of riser k + top.append(((k + 1) * A, Z0 + (k + 1) * S)) # end of tread k + # top endet bei (N*A, Z0 + N*S) + x_end = N * A + z_top = Z0 + N * S + + if modus == "massiv": + return top + [(x_end, Z0), (0.0, Z0)] + + if modus == "flach": + # Soffit parallel zur Steigungslinie (von (0,Z0) bis (NA, NS+Z0)), + # vertikal um lauf_d nach unten versetzt. + return top + [(x_end, z_top - d), + (0.0, Z0 - d), + (0.0, Z0)] + + # plattenrand: Faltwerk-Treppe. Innere (Soffit-) Punkte mit Konvex/ + # Konkav-Handling, sodass die Vertikalen unter den Setzstufen ihre + # eigene D-Tiefe haben (kein Klotz-Look mit floatenden Tritten). + inner = [] + inner.append((d, Z0)) # Start: rechts vom Riser-Anfang (Konvex-Ecke (0,Z0)) + for k in range(N): + # Konvex-Ecke (k*A, Z0+(k+1)*S): Top-Eck rechts oben des Risers + # Innere Eck-Punkt bei (k*A + D, (k+1)S - D) + inner.append((k * A + d, Z0 + (k + 1) * S - d)) + # Konkav-Ecke ((k+1)*A, Z0+(k+1)*S): Tread-Ende, naechster Riser + # geht hoch. Extra L-Verlaengerung — ausser beim allerletzten Schritt, + # wo der Tritt zur Decke laeuft. + if k < N - 1: + inner.append(((k + 1) * A, Z0 + (k + 1) * S - d)) + inner.append(((k + 1) * A + d, Z0 + (k + 1) * S - d)) + else: + inner.append((x_end, Z0 + N * S - d)) + # Schliessen: Top-right Drop + Inner reversed + zurueck zum Start + # inner letzter Punkt ist (x_end, z_top - d). Top-right ist (x_end, z_top). + inner_rev = list(reversed(inner)) # endet bei (d, Z0) + return top + inner_rev + [(0.0, Z0)] + + +def _wendel_radii(r_click, breite, referenz): + """Berechnet (r_inner, r_outer) der Wendeltreppe basierend auf dem + Klick-Radius (= Lauflinien-Position) und der Referenz. + Konvention: + 'links' → Lauflinie auf AUSSEN-Kante (body extends inward) + 'mid' → Lauflinie mittig + 'rechts' → Lauflinie auf INNEN-Kante (body extends outward) + + r_inner wird absolut auf >= 0.01m geclampt. Bei r_inner < 0.05m + schaltet die Geometrie auf Cone-Wedge um (Innenkante kollabiert + zur Mittelachse — Spindeltreppe-Style).""" + half_b = float(breite) * 0.5 + MIN_R = 0.01 + if referenz == "links": + return (max(MIN_R, r_click - float(breite)), r_click) + if referenz == "rechts": + return (max(MIN_R, r_click), r_click + float(breite)) + return (max(MIN_R, r_click - half_b), r_click + half_b) + + +def _wendel_sweep(center, p_start, p_end): + """Liefert (alpha_start, delta) — Startwinkel und signed Sweep-Winkel + in Rad. Sweep-Richtung kommt aus dem Cross-Product start vs. end.""" + import math + sx, sy = p_start.X - center.X, p_start.Y - center.Y + ex, ey = p_end.X - center.X, p_end.Y - center.Y + a_start = math.atan2(sy, sx) + a_end_raw = math.atan2(ey, ex) + cross_z = sx * ey - sy * ex + sweep_sign = 1.0 if cross_z >= 0 else -1.0 + delta = a_end_raw - a_start + if sweep_sign > 0: + while delta < 0: delta += 2.0 * math.pi + else: + while delta > 0: delta -= 2.0 * math.pi + return a_start, delta + + +def _wendel_wedge_cone_brep(center, r_out, a0, a1, top_z, + bot_z_a0, bot_z_a1, tol=0.001): + """Cone-Wedge fuer Spindeltreppen: innere Kante kollabiert zur + Mittelachse. 5 Vertices (t_c, t_o0, t_o1, b_c, b_o0, b_o1) und + 5-6 Faces (Top-Dreieck, Bottom-Dreieck, Aussen-Quad, 2 Radial- + Quads). Vermeidet degenerierte Innen-Faces bei r_inner ≈ 0.""" + import math + cx, cy = center.X, center.Y + t_c = rg.Point3d(cx, cy, top_z) + t_o0 = rg.Point3d(cx + r_out * math.cos(a0), cy + r_out * math.sin(a0), top_z) + t_o1 = rg.Point3d(cx + r_out * math.cos(a1), cy + r_out * math.sin(a1), top_z) + b_o0 = rg.Point3d(cx + r_out * math.cos(a0), cy + r_out * math.sin(a0), bot_z_a0) + b_o1 = rg.Point3d(cx + r_out * math.cos(a1), cy + r_out * math.sin(a1), bot_z_a1) + # Bottom-Center: bei flach-Modus = Mittel der beiden bot_z, sonst gleich. + b_c_z = (bot_z_a0 + bot_z_a1) * 0.5 + b_c = rg.Point3d(cx, cy, b_c_z) + + da = a1 - a0 + flat_bot = abs(bot_z_a0 - bot_z_a1) < 1e-6 + faces = [] + # Top Dreieck — CCW von oben + if da > 0: + faces.append([t_c, t_o0, t_o1, t_c]) + else: + faces.append([t_c, t_o1, t_o0, t_c]) + # Bottom Dreieck — reversed + if da > 0: + faces.append([b_c, b_o1, b_o0, b_c]) + else: + faces.append([b_c, b_o0, b_o1, b_c]) + # Aussen-Seite (planar bei flat_bot, sonst trianguliert) + if flat_bot: + faces.append([t_o0, b_o0, b_o1, t_o1, t_o0]) + else: + faces.append([t_o0, b_o0, b_o1, t_o0]) + faces.append([t_o0, b_o1, t_o1, t_o0]) + # Radiale Seiten (immer planar — alle in radial Ebene) + faces.append([t_c, t_o0, b_o0, b_c, t_c]) + faces.append([t_c, b_c, b_o1, t_o1, t_c]) + + breps = [] + for face_pts in faces: + try: + f = _planar_face_from_pts(face_pts, tol) + if f: breps.append(f) + except Exception: pass + if not breps: return None + try: + joined = rg.Brep.JoinBreps(breps, tol) + if joined and len(joined) > 0: return joined[0] + except Exception: pass + return breps[0] if breps else None + + +def _wendel_wedge_brep(center, r_in, r_out, a0, a1, top_z, + bot_z_a0, bot_z_a1, tol=0.001): + """Bauet einen Wendel-Tritt als 8-Vertex-Polyeder: + - Flat top bei top_z + - Bottom Z-Werte koennen pro Winkel-Seite differieren (flach-Modus: + schraeg parallel zur Steigungslinie; plattenrand-Modus: flat). + - 4 Seitenflaechen (innen, aussen, radial a0, radial a1). + + Bei r_in < 0.05m → Cone-Wedge (Spindeltreppe-Style) — verhindert + degenerierte Geometrie an der Mittelachse.""" + import math + if r_in < 0.05: + return _wendel_wedge_cone_brep(center, r_out, a0, a1, top_z, + bot_z_a0, bot_z_a1, tol) + cx, cy = center.X, center.Y + t_i0 = rg.Point3d(cx + r_in * math.cos(a0), cy + r_in * math.sin(a0), top_z) + t_o0 = rg.Point3d(cx + r_out * math.cos(a0), cy + r_out * math.sin(a0), top_z) + t_i1 = rg.Point3d(cx + r_in * math.cos(a1), cy + r_in * math.sin(a1), top_z) + t_o1 = rg.Point3d(cx + r_out * math.cos(a1), cy + r_out * math.sin(a1), top_z) + b_i0 = rg.Point3d(cx + r_in * math.cos(a0), cy + r_in * math.sin(a0), bot_z_a0) + b_o0 = rg.Point3d(cx + r_out * math.cos(a0), cy + r_out * math.sin(a0), bot_z_a0) + b_i1 = rg.Point3d(cx + r_in * math.cos(a1), cy + r_in * math.sin(a1), bot_z_a1) + b_o1 = rg.Point3d(cx + r_out * math.cos(a1), cy + r_out * math.sin(a1), bot_z_a1) + + da = a1 - a0 + flat_bot = abs(bot_z_a0 - bot_z_a1) < 1e-6 + faces = [] + # Top — planar + if da > 0: + faces.append([t_i0, t_o0, t_o1, t_i1, t_i0]) + else: + faces.append([t_i0, t_i1, t_o1, t_o0, t_i0]) + # Bottom — planar wenn flat, sonst in 2 Dreiecke teilen + if da > 0: + bot_order = [b_i0, b_i1, b_o1, b_o0] + else: + bot_order = [b_i0, b_o0, b_o1, b_i1] + if flat_bot: + faces.append(bot_order + [bot_order[0]]) + else: + faces.append([bot_order[0], bot_order[1], bot_order[2], bot_order[0]]) + faces.append([bot_order[0], bot_order[2], bot_order[3], bot_order[0]]) + # Inner side: planar wenn flat, sonst Dreiecke + if flat_bot: + faces.append([t_i0, t_i1, b_i1, b_i0, t_i0]) + else: + faces.append([t_i0, t_i1, b_i1, t_i0]) + faces.append([t_i0, b_i1, b_i0, t_i0]) + # Outer side: planar wenn flat, sonst Dreiecke + if flat_bot: + faces.append([t_o0, b_o0, b_o1, t_o1, t_o0]) + else: + faces.append([t_o0, b_o0, b_o1, t_o0]) + faces.append([t_o0, b_o1, t_o1, t_o0]) + # Radiale Seiten — immer planar (alle Punkte auf derselben Radial-Ebene) + faces.append([t_i0, t_o0, b_o0, b_i0, t_i0]) + faces.append([t_i1, b_i1, b_o1, t_o1, t_i1]) + + breps = [] + for face_pts in faces: + try: + f = _planar_face_from_pts(face_pts, tol) + if f: breps.append(f) + except Exception: pass + if not breps: return None + try: + joined = rg.Brep.JoinBreps(breps, tol) + if joined and len(joined) > 0: + return joined[0] + except Exception as ex: + print("[ELEMENTE] wendel wedge join:", ex) + return breps[0] if breps else None + + +def _wendel_sweep_range(r_lauf, breite, referenz, n_stufen, H, soll): + """Liefert (sweep_min, sweep_max) in Radian — gueltiger Drehwinkel. + Erzwingt Soll-Auftritt-Range UEBER DIE GANZE TRITTBREITE (von + r_inner bis r_outer), nicht nur an der Lauflinie: + A = r * (sweep/N) + A_inner = r_inner * (sweep/N) >= a_lo → sweep >= N*a_lo/r_inner + A_outer = r_outer * (sweep/N) <= a_hi → sweep <= N*a_hi/r_outer + + So liegen beide Enden eines Tritts im Soll. Wenn der Bereich + widerspruechlich ist (zu breite Treppe fuer kleinen Radius), fallback + auf Lauflinie-basiertes Clamping.""" + r_inner, r_outer = _wendel_radii(r_lauf, breite, referenz) + n = max(1, int(n_stufen)) + s = float(H) / n + a_lo, a_hi = 0.05, 2.0 + if soll.get("sa", [0, 0, False])[2]: + a_lo = max(a_lo, float(soll["sa"][0]) - 2.0 * s) + a_hi = min(a_hi, float(soll["sa"][1]) - 2.0 * s) + if soll.get("a", [0, 0, False])[2]: + a_lo = max(a_lo, float(soll["a"][0])) + a_hi = min(a_hi, float(soll["a"][1])) + if a_lo > a_hi: + mid = (a_lo + a_hi) * 0.5 + a_lo = a_hi = mid + ri = max(0.01, float(r_inner)) + ro = max(0.01, float(r_outer)) + sweep_lo = n * a_lo / ri # tightest lower (kleinster Radius) + sweep_hi = n * a_hi / ro # tightest upper (groesster Radius) + if sweep_lo > sweep_hi: + # Range nicht erfuellbar (zu breite Treppe oder zu enger Radius) + # → Fallback: nur an der Lauflinie clampen, der User sieht im Label + # dass A_inner/A_outer ausserhalb Soll sind. + rl = max(0.01, float(r_lauf)) + sweep_lo = n * a_lo / rl + sweep_hi = n * a_hi / rl + return (sweep_lo, sweep_hi) + + +def _make_treppe_wendel_volume(axis_polyline, breite, referenz, n_stufen, + uk, ok, modus="flach", lauf_d=0.18): + """Wendeltreppe aus 3-Punkt-Polylinie [center, start, end]. + + Bauet N Stufen als gestapelte trapezfoermige Keile um die Mittelachse. + Lauflinien-Radius = Distanz center→start. Sweep-Winkel und -Richtung + werden aus der End-Position abgeleitet (Cross-Product gibt Drehsinn, + Winkel ist die natuerliche Strecke in dieser Richtung). + + Modus: + 'massiv' — jeder Keil reicht von UK bis Step-Top (Wedding-Cake) + 'flach' / 'plattenrand' — jeder Keil ist nur lauf_d dick (floating + steps); bei Wendel keine echte Helix-Soffit fuer Entwurfs-Niveau.""" + import math + if not isinstance(axis_polyline, rg.Curve): return None + try: + ok_pl, poly = axis_polyline.TryGetPolyline() + except Exception: return None + if not ok_pl or poly is None or poly.Count != 3: return None + center = rg.Point3d(poly[0].X, poly[0].Y, 0) + p_start = rg.Point3d(poly[1].X, poly[1].Y, 0) + p_end = rg.Point3d(poly[2].X, poly[2].Y, 0) + r_click = math.sqrt((p_start.X - center.X) ** 2 + + (p_start.Y - center.Y) ** 2) + if r_click < 0.2: return None + r_inner, r_outer = _wendel_radii(r_click, breite, referenz) + if r_outer - r_inner < 0.05: return None + a_start, delta = _wendel_sweep(center, p_start, p_end) + if abs(delta) < 0.05: return None + H = float(ok) - float(uk) + if H <= 1e-6: return None + N = max(2, int(n_stufen)) + S = H / N + da = delta / N + + parts = [] + for k in range(N): + a0 = a_start + k * da + a1 = a_start + (k + 1) * da + z_top = float(uk) + (k + 1) * S + if modus == "massiv": + # Wedding-Cake: Block bis zum Boden, einfache Extrusion + z_bot = float(uk) + c0i = (center.X + r_inner * math.cos(a0), center.Y + r_inner * math.sin(a0)) + c0o = (center.X + r_outer * math.cos(a0), center.Y + r_outer * math.sin(a0)) + c1i = (center.X + r_inner * math.cos(a1), center.Y + r_inner * math.sin(a1)) + c1o = (center.X + r_outer * math.cos(a1), center.Y + r_outer * math.sin(a1)) + if da > 0: corners = [c0i, c0o, c1o, c1i] + else: corners = [c0i, c1i, c1o, c0o] + pts = [rg.Point3d(x, y, z_bot) for (x, y) in corners] + pts.append(pts[0]) + try: + crv = rg.PolylineCurve(rg.Polyline(pts)) + ext = rg.Extrusion.Create(crv, z_top - z_bot, True) + if ext is not None: parts.append(ext.ToBrep()) + except Exception as ex: + print("[ELEMENTE] Wendel massiv step:", ex) + elif modus == "plattenrand": + # Gestuft: flat bottom bei z_top - D unter jedem Tritt. + # Adjacent Wedges haben unterschiedliche Bottom-Z → visible + # "Schritt" auf der Unterseite (= Faltwerk-Look). + bot = z_top - float(lauf_d) + wedge = _wendel_wedge_brep(center, r_inner, r_outer, + a0, a1, z_top, bot, bot) + if wedge is not None: parts.append(wedge) + else: # flach + # Helikoide Unterseite: Bottom verlaeuft schraeg parallel zur + # Steigungslinie. Bei a0 bei (uk + k*S - D), bei a1 bei + # (uk + (k+1)*S - D). Adjacent Wedges meet seamlessly an der + # gemeinsamen Kante → kontinuierlicher Spiral-Slab. + bot_a0 = float(uk) + k * S - float(lauf_d) + bot_a1 = float(uk) + (k + 1) * S - float(lauf_d) + wedge = _wendel_wedge_brep(center, r_inner, r_outer, + a0, a1, z_top, bot_a0, bot_a1) + if wedge is not None: parts.append(wedge) + + if not parts: return None + if len(parts) == 1: return parts[0] + try: + merged = rg.Brep.MergeBreps(parts, 0.001) + if merged is not None: return merged + except Exception: pass + try: + joined = rg.Brep.JoinBreps(parts, 0.001) + if joined and len(joined) > 0: return joined[0] + except Exception: pass + return parts[0] + + +def _line_intersect_xy(p1, dir1, p2, dir2): + """2D-Linien-Schnittpunkt in der XY-Ebene. dir1, dir2 = Richtungs- + vektoren (muessen nicht normalisiert sein). Liefert Point3d (Z=0) + oder None bei Parallelitaet.""" + det = dir1.X * (-dir2.Y) - dir1.Y * (-dir2.X) + if abs(det) < 1e-9: return None + dx = p2.X - p1.X + dy = p2.Y - p1.Y + t1 = (dx * (-dir2.Y) - dy * (-dir2.X)) / det + return rg.Point3d(p1.X + dir1.X * t1, p1.Y + dir1.Y * t1, 0) + + +def _make_treppe_l_volume(axis_polyline, breite, referenz, n_stufen, uk, ok, + modus="flach", lauf_d=0.18): + """L-Treppe aus 3-Punkt-Polylinie (Start, Eck-Punkt, End). + Bauet 2 gerade Laufe + 1 Podest am Eck zusammen. Hoehe wird + proportional zu den Lauflinien-Laengen auf die beiden Laufe verteilt.""" + if not isinstance(axis_polyline, rg.Curve): return None + try: + ok_pl, poly = axis_polyline.TryGetPolyline() + except Exception: + return None + if not ok_pl or poly is None or poly.Count != 3: + return None + p0 = rg.Point3d(poly[0].X, poly[0].Y, 0) + p1 = rg.Point3d(poly[1].X, poly[1].Y, 0) + p2 = rg.Point3d(poly[2].X, poly[2].Y, 0) + + v1 = rg.Vector3d(p1.X - p0.X, p1.Y - p0.Y, 0) + v2 = rg.Vector3d(p2.X - p1.X, p2.Y - p1.Y, 0) + L1 = v1.Length + L2 = v2.Length + half_b = float(breite) * 0.5 + if L1 < half_b + 0.05 or L2 < half_b + 0.05: + print("[ELEMENTE] L-Treppe: Lauflinien zu kurz fuer Podest") + return None + + H = float(ok) - float(uk) + if H <= 1e-6: return None + N = max(2, int(n_stufen)) + S = H / N + + # Stufen-Verteilung: N1 wird aus L1 mit dem optimalen A bestimmt, + # damit die Klick-Position des Users direkt N1 (Stufen vor Podest) + # entspricht — genauso wie's der Live-Preview anzeigt. + eff_L1 = L1 - half_b + eff_L2 = L2 - half_b + if eff_L1 + eff_L2 <= 0: return None + A_opt = 0.63 - 2.0 * S + if A_opt < 0.21: A_opt = 0.21 + if A_opt > 0.35: A_opt = 0.35 + N1 = int(round(eff_L1 / A_opt)) + if N1 < 1: N1 = 1 + if N1 > N - 1: N1 = N - 1 + N2 = N - N1 + + v1u = rg.Vector3d(v1); v1u.Unitize() + v2u = rg.Vector3d(v2); v2u.Unitize() + + # Run 1: von p0 bis p1 - v1u*half_b + run1_end = rg.Point3d(p1.X - v1u.X * half_b, p1.Y - v1u.Y * half_b, 0) + line1 = rg.LineCurve(p0, run1_end) + z_podest = float(uk) + N1 * S + brep1 = _make_treppe_volume(line1, breite, referenz, N1, + float(uk), z_podest, modus, lauf_d) + + # Run 2: von p1 + v2u*half_b bis p2 + run2_start = rg.Point3d(p1.X + v2u.X * half_b, p1.Y + v2u.Y * half_b, 0) + line2 = rg.LineCurve(run2_start, p2) + brep2 = _make_treppe_volume(line2, breite, referenz, N2, + z_podest, float(ok), modus, lauf_d) + + # Podest am Eck p1 — adaptives Hexagon das die zwei Lauf-Querschnitte + # an ihren tatsaechlichen Richtungen verbindet. Bei 90° L kollabiert + # zu Quadrat, bei flacheren/spitzeren Winkeln wird's ein 5/6-Eck mit + # den Schnittpunkten der ausserhalbliegenden Seitenwaende. + perp1 = rg.Vector3d(-v1u.Y, v1u.X, 0) + perp2 = rg.Vector3d(-v2u.Y, v2u.X, 0) + b = float(breite) + if referenz == "links": + perp_lo, perp_hi = 0.0, +b + elif referenz == "rechts": + perp_lo, perp_hi = -b, 0.0 + else: # mid + perp_lo, perp_hi = -half_b, +half_b + + # End-Querschnitt von Run 1 + Start-Querschnitt von Run 2 + end_lo = rg.Point3d(run1_end.X + perp1.X * perp_lo, + run1_end.Y + perp1.Y * perp_lo, 0) + end_hi = rg.Point3d(run1_end.X + perp1.X * perp_hi, + run1_end.Y + perp1.Y * perp_hi, 0) + start_lo = rg.Point3d(run2_start.X + perp2.X * perp_lo, + run2_start.Y + perp2.Y * perp_lo, 0) + start_hi = rg.Point3d(run2_start.X + perp2.X * perp_hi, + run2_start.Y + perp2.Y * perp_hi, 0) + # Eckpunkte = Schnitt der Seitenwand-Linien (Run 1 weiter entlang v1, + # Run 2 zurueck entlang -v2) + minus_v2 = rg.Vector3d(-v2u.X, -v2u.Y, 0) + corner_lo = _line_intersect_xy(end_lo, v1u, start_lo, minus_v2) + corner_hi = _line_intersect_xy(end_hi, v1u, start_hi, minus_v2) + + if modus == "massiv": + z_lo = float(uk) + else: + z_lo = z_podest - float(lauf_d) + z_hi = z_podest + + podest_brep = None + try: + # Hexagon-Vertices in CCW-Order: + # end_lo → corner_lo → start_lo → start_hi → corner_hi → end_hi + def _add_unique(arr, p, tol=1e-5): + if p is None: return + if not arr: arr.append(p); return + last = arr[-1] + if (last.X - p.X) ** 2 + (last.Y - p.Y) ** 2 < tol * tol: return + arr.append(p) + + verts = [] + _add_unique(verts, end_lo) + _add_unique(verts, corner_lo) + _add_unique(verts, start_lo) + _add_unique(verts, start_hi) + _add_unique(verts, corner_hi) + _add_unique(verts, end_hi) + + if len(verts) >= 3: + # CCW-Check via Shoelace — sonst umdrehen damit Extrusion in +Z geht + area2 = 0.0 + n_v = len(verts) + for i in range(n_v): + pa = verts[i] + pb = verts[(i + 1) % n_v] + area2 += pa.X * pb.Y - pa.Y * pb.X + if area2 < 0: + verts = list(reversed(verts)) + pts_bot = [rg.Point3d(p.X, p.Y, z_lo) for p in verts] + pts_bot.append(pts_bot[0]) + bot_curve = rg.PolylineCurve(rg.Polyline(pts_bot)) + ext = rg.Extrusion.Create(bot_curve, z_hi - z_lo, True) + if ext is not None: + podest_brep = ext.ToBrep() + except Exception as ex: + print("[ELEMENTE] Podest hexagon:", ex) + + parts = [b for b in (brep1, podest_brep, brep2) if b is not None] + if not parts: return None + if len(parts) == 1: return parts[0] + # MergeBreps versucht alle Brep-Teile zu einem einzelnen Brep + # (mit ggf. mehreren disjunkten Shells) zu kombinieren. + try: + merged = rg.Brep.MergeBreps(parts, 0.001) + if merged is not None: return merged + except Exception: pass + try: + joined = rg.Brep.JoinBreps(parts, 0.001) + if joined and len(joined) > 0: return joined[0] + except Exception: pass + return parts[0] + + +def _make_treppe_volume(axis_curve, breite, referenz, n_stufen, uk, ok, + modus="flach", lauf_d=0.18): + """Gerade Treppe: bauet ein Seitenprofil (Step-Polygon) entlang der + Lauflinie und extrudiert es senkrecht um `breite`. Einzelnes sauberes + Brep-Volumen. `modus` bestimmt die Form der Unterseite.""" + if not isinstance(axis_curve, rg.Curve): return None + try: + P0 = axis_curve.PointAtStart + P1 = axis_curve.PointAtEnd + tan_vec = rg.Vector3d(P1.X - P0.X, P1.Y - P0.Y, 0) + L = tan_vec.Length + if L < 1e-6: return None + tan_vec.Unitize() + perp = rg.Vector3d(-tan_vec.Y, tan_vec.X, 0) + H = float(ok) - float(uk) + if H <= 1e-6: return None + N = max(2, int(n_stufen)) + S = H / N + A = L / N # N Auftritte (oberster Tritt inkl.) + + if modus not in _TREPPE_MODI: modus = "flach" + profile_2d = _treppe_profile_2d(N, S, A, uk, modus, lauf_d) + + b = float(breite) + if referenz == "links": + perp_start, perp_end = 0.0, -b + elif referenz == "rechts": + perp_start, perp_end = 0.0, +b + else: + perp_start, perp_end = -b * 0.5, +b * 0.5 + + shift0 = rg.Vector3d(perp.X * perp_start, perp.Y * perp_start, 0) + world_pts = [] + for (px, pz) in profile_2d: + wp = rg.Point3d(P0.X + tan_vec.X * px + shift0.X, + P0.Y + tan_vec.Y * px + shift0.Y, + pz) + world_pts.append(wp) + poly = rg.Polyline(world_pts) + profile_curve = rg.PolylineCurve(poly) + if not profile_curve.IsClosed: return None + + extrude_len = perp_end - perp_start + try: + ext = rg.Extrusion.Create(profile_curve, extrude_len, True) + if ext is not None: + return ext.ToBrep() + except Exception as ex: + print("[ELEMENTE] Treppe Extrusion:", ex) + return None + except Exception as ex: + print("[ELEMENTE] _make_treppe_volume:", ex) + return None + + +def _regenerate_element(doc, element_id): + """Regeneriert das Volumen eines Elements (Wand oder Decke) anhand + seines Source-Objekts (Achse bzw. Outline).""" + src_obj, meta = _find_source(doc, element_id) + if src_obj is None or meta is None: return False + # Oeffnung selbst hat kein Volumen — stattdessen die Elternwand regen + if meta["type"] == "oeffnung_point": + parent_id = meta.get("oeff_parent") or "" + if parent_id: + return _regenerate_element(doc, parent_id) + return False + geom = src_obj.Geometry + if not isinstance(geom, rg.Curve): return False + g = _geschoss_by_id(doc, meta["geschoss"]) + geschoss_name = g.get("name", "EG") if g else "EG" + + # _REGEN_BUSY waehrend Regen setzen — verhindert dass die Listener + # (Add/Replace/Delete) waehrend dem Erstellen/Loeschen von Volume- + # Objekten Dedup oder Cascade-Logik ausfuehren. Wichtig fuer + # Oeffnungen, wo mehrere Brep-Pieces mit gleicher ID hinzugefuegt + # werden. + _was_busy = sc.sticky.get(_REGEN_BUSY, False) + sc.sticky[_REGEN_BUSY] = True + try: + return _regenerate_element_body(doc, element_id, src_obj, meta, + geom, geschoss_name) + finally: + sc.sticky[_REGEN_BUSY] = _was_busy + + +def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name): + """Eigentliche Implementierung des Regen — der aeussere Wrapper + `_regenerate_element` setzt _REGEN_BUSY und dispatcht oeffnung_point.""" + if meta["type"] == "wand_axis": + uk, ok = _resolve_uk_ok(doc, meta["geschoss"], + meta["uk_override"], meta["ok_override"]) + brep = _make_volume_geometry(geom, meta["dicke"], uk, ok, + meta.get("referenz", "mid")) + # Oeffnungen (Fenster/Tueren) abziehen + jeweils das Oeffnungs- + # Volumen (Rahmen+Sims+Glas) erstellen oder aktualisieren. + opening_jobs = [] + if brep is not None: + for op_obj, op_meta in _find_openings_for_wall(doc, element_id): + pt_geom = op_obj.Geometry + pt_loc = None + if hasattr(pt_geom, 'Location'): + pt_loc = pt_geom.Location + elif isinstance(pt_geom, rg.Point3d): + pt_loc = pt_geom + if pt_loc is None: continue + # Effektives Oeffnungs-Zentrum auf der Achse je nach + # Referenz-Lage (mid/links/rechts) berechnen. + eff_pt = _oeff_effective_axis_point( + geom, pt_loc, op_meta["oeff_breite"], + op_meta.get("oeff_referenz", "mid")) + cutout = _make_oeffnung_cutout( + geom, eff_pt, meta["dicke"], + op_meta["oeff_breite"], op_meta["oeff_hoehe"], + op_meta["oeff_brueest"], uk) + if cutout is None: continue + try: + diff = rg.Brep.CreateBooleanDifference( + [brep], [cutout], 0.001) + if diff and len(diff) > 0: + brep = diff[0] + except Exception as ex: + print("[ELEMENTE] BoolDiff Oeffnung:", ex) + # Job fuer das Oeffnungs-Volumen merken — mit effektivem + # Zentrum, sodass der Rahmen am selben Ort entsteht. + opening_jobs.append((op_meta, eff_pt, uk)) + # Oeffnungs-Volumina aktualisieren (Rahmen + Mittelpfosten + Sims + Glas). + # Mehrere Brep-Pieces pro Oeffnung — alle bekommen die gleiche + # Oeffnungs-ID und werden bei jedem Regen komplett neu aufgebaut. + op_layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name)) + for op_meta, pt_loc, op_uk in opening_jobs: + # Alte Volume-Objekte dieser Oeffnung loeschen + for o, _m in _find_objects_by_wall_id(doc, op_meta["id"], "oeffnung_volume"): + try: doc.Objects.Delete(o.Id, True) + except Exception as ex: print("[ELEMENTE] del old oeff vol:", ex) + # Neue Pieces bauen + pieces = _make_oeffnung_pieces(geom, pt_loc, meta["dicke"], + op_meta, op_uk) + for pbrep in pieces: + op_attrs = Rhino.DocObjects.ObjectAttributes() + op_attrs.LayerIndex = op_layer + _attach_meta(op_attrs, op_meta["id"], "oeffnung_volume", + op_meta["geschoss"], meta["dicke"], "", "", + oeff_typ=op_meta.get("oeff_typ"), + oeff_parent=op_meta.get("oeff_parent"), + oeff_breite=op_meta.get("oeff_breite"), + oeff_hoehe=op_meta.get("oeff_hoehe"), + oeff_brueest=op_meta.get("oeff_brueest"), + oeff_rahmen_b=op_meta.get("oeff_rahmen_b"), + oeff_rahmen_tiefe=op_meta.get("oeff_rahmen_tiefe"), + oeff_rahmen_pos=op_meta.get("oeff_rahmen_pos"), + oeff_fluegel=op_meta.get("oeff_fluegel"), + oeff_sims_aus=op_meta.get("oeff_sims_aus"), + oeff_sims_in=op_meta.get("oeff_sims_in"), + oeff_glas=op_meta.get("oeff_glas"), + oeff_referenz=op_meta.get("oeff_referenz")) + doc.Objects.AddBrep(pbrep, op_attrs) + vol_type = "wand_volume" + layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name)) + src_layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) + elif meta["type"] == "decke_outline": + uk, ok = _resolve_decke_z(doc, meta["geschoss"], meta["dicke"], + meta["uk_override"], meta["ok_override"]) + brep = _make_decke_volume(geom, meta["dicke"], uk, ok) + vol_type = "decke_volume" + layer = _ensure_layer(doc, _layer_path_decke(doc, geschoss_name)) + src_layer = layer + elif meta["type"] == "dach_outline": + base = _resolve_dach_base(doc, meta["geschoss"], meta["uk_override"]) + dt = meta.get("dach_typ", "pult") + if dt == "sattel": + brep = _make_satteldach_brep(geom, meta["dicke"], base, + meta.get("neigung", 30.0)) + elif dt == "walm": + brep = _make_walmdach_brep(geom, meta["dicke"], base, + meta.get("neigung", 30.0)) + elif dt == "mansarde": + brep = _make_mansardendach_brep( + geom, meta["dicke"], base, + meta.get("neigung", 30.0), + meta.get("neigung_unten", 60.0), + meta.get("knick_h", 2.0), + variante=meta.get("dach_variante", "walm")) + else: # pult + brep = _make_pultdach_volume(geom, meta["dicke"], base, + meta.get("neigung", 30.0), + meta.get("eave_idx", 0)) + vol_type = "dach_volume" + layer = _ensure_layer(doc, _layer_path_dach(doc, geschoss_name)) + src_layer = layer + elif meta["type"] == "treppe_axis": + # Start- und Zielgeschoss → uk/ok aus OKFF-Differenz. + # H-Override hat Vorrang vor Zielgeschoss. + g_start = _geschoss_by_id(doc, meta["geschoss"]) + g_end = _geschoss_by_id(doc, meta.get("geschoss_end", "")) + if g_start is None: + return False + uk = float(g_start.get("okff", 0.0)) + h_over = meta.get("treppe_h_over", "") + if h_over: + try: + ok = uk + float(h_over) + except Exception: + ok = uk + float(g_start.get("hoehe", 3.0)) + elif g_end is not None: + ok = float(g_end.get("okff", uk + 3.0)) + else: + ok = uk + float(g_start.get("hoehe", 3.0)) + art = meta.get("treppe_art", "gerade") + if art == "l": + brep = _make_treppe_l_volume(geom, meta.get("treppe_breite", 1.0), + meta.get("treppe_referenz", "mid"), + meta.get("treppe_n", 15), uk, ok, + modus=meta.get("treppe_modus", "flach"), + lauf_d=meta.get("treppe_lauf_d", 0.18)) + elif art == "wendel": + brep = _make_treppe_wendel_volume( + geom, meta.get("treppe_breite", 1.0), + meta.get("treppe_referenz", "mid"), + meta.get("treppe_n", 15), uk, ok, + modus=meta.get("treppe_modus", "flach"), + lauf_d=meta.get("treppe_lauf_d", 0.18)) + else: + brep = _make_treppe_volume(geom, meta.get("treppe_breite", 1.0), + meta.get("treppe_referenz", "mid"), + meta.get("treppe_n", 15), uk, ok, + modus=meta.get("treppe_modus", "flach"), + lauf_d=meta.get("treppe_lauf_d", 0.18)) + vol_type = "treppe_volume" + layer = _ensure_layer(doc, _layer_path_treppe(doc, geschoss_name)) + src_layer = layer + else: + return False + + # Migration: Source-Objekt (Achse/Outline) auf den aktuellen Layer + # schieben, falls es noch auf einem alten Layer steht (z.B. von einer + # frueheren Bug-Version auf "01_WAND" / "06_3D_VOLUMEN") + try: + if src_layer >= 0 and src_obj.Attributes.LayerIndex != src_layer: + new_attrs = src_obj.Attributes.Duplicate() + new_attrs.LayerIndex = src_layer + doc.Objects.ModifyAttributes(src_obj, new_attrs, True) + except Exception as ex: + print("[ELEMENTE] migrate src-layer:", ex) + + if brep is None: return False + vol_obj = _find_target_volume(doc, element_id) + attrs = Rhino.DocObjects.ObjectAttributes() + attrs.LayerIndex = layer + _attach_meta(attrs, element_id, vol_type, meta["geschoss"], + meta["dicke"], meta["uk_override"], meta["ok_override"], + meta.get("referenz", "mid"), + neigung=meta.get("neigung"), + eave_idx=meta.get("eave_idx"), + dach_typ=meta.get("dach_typ"), + neigung_unten=meta.get("neigung_unten"), + knick_h=meta.get("knick_h"), + dach_variante=meta.get("dach_variante"), + geschoss_end=meta.get("geschoss_end"), + treppe_breite=meta.get("treppe_breite"), + treppe_n=meta.get("treppe_n"), + treppe_referenz=meta.get("treppe_referenz"), + treppe_modus=meta.get("treppe_modus"), + treppe_lauf_d=meta.get("treppe_lauf_d"), + treppe_art=meta.get("treppe_art"), + treppe_h_over=meta.get("treppe_h_over"), + treppe_soll=meta.get("treppe_soll")) + if vol_obj is not None: + doc.Objects.Replace(vol_obj.Id, brep) + vol_obj_new = doc.Objects.Find(vol_obj.Id) + if vol_obj_new is not None: + vol_obj_new.Attributes = attrs + vol_obj_new.CommitChanges() + else: + doc.Objects.AddBrep(brep, attrs) + return True + + +# Alias fuer Backwards-Compat / interne Aufrufer +_regenerate_volume = _regenerate_element + + +# --- Bridge ----------------------------------------------------------------- + +class ElementeBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "elemente") + self._last_selection_ids = () + + def _on_ready(self): + self._send_state() + + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): p = {} + + if t == "READY": self._on_ready() + elif t == "LIST": self._send_state() + elif t == "CREATE_WALL": self._cmd_create_wall(p) + elif t == "CREATE_DECKE": self._cmd_create_decke(p) + elif t == "CREATE_DACH": self._cmd_create_dach(p) + elif t == "CREATE_FENSTER": self._cmd_create_oeffnung(p, "fenster") + elif t == "CREATE_TUER": self._cmd_create_oeffnung(p, "tuer") + elif t == "CREATE_TREPPE": self._cmd_create_treppe(p) + elif t == "UPDATE_WALL": self._update_wall(p) + elif t == "UPDATE_ELEMENT": self._update_wall(p) # gleiche Logik fuer alle + elif t == "DELETE_WALL": self._delete_wall(p.get("id")) + elif t == "DELETE_ELEMENT": self._delete_wall(p.get("id")) + elif t == "REGENERATE_ALL": self._regenerate_all() + + def _send_state(self): + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: + self.send("STATE", {"elements": [], "geschosse": [], "selection": None}) + return + geschosse = _load_geschosse(doc) + # Alle Source-Objekte (Achsen + Outlines) durchgehen + elements = [] + seen_ids = set() + for obj in doc.Objects: + meta = _read_meta(obj) + if meta is None: continue + if meta["type"] not in SOURCE_TYPES: continue + if meta["id"] in seen_ids: continue + seen_ids.add(meta["id"]) + g = _geschoss_by_id(doc, meta["geschoss"]) + geschoss_name = g.get("name", "?") if g else "?" + selected = obj.IsSelected(False) > 0 + base = { + "id": meta["id"], + "objectId": str(obj.Id), + "geschoss": meta["geschoss"], + "geschossName": geschoss_name, + "dicke": meta["dicke"], + "ukOverride": meta["uk_override"], + "okOverride": meta["ok_override"], + "selected": selected, + } + if meta["type"] == "wand_axis": + uk, ok = _resolve_uk_ok(doc, meta["geschoss"], + meta["uk_override"], meta["ok_override"]) + base.update({ + "kind": "wand", + "referenz": meta.get("referenz", "mid"), + "uk": uk, + "ok": ok, + }) + elif meta["type"] == "decke_outline": + uk, ok = _resolve_decke_z(doc, meta["geschoss"], meta["dicke"], + meta["uk_override"], meta["ok_override"]) + base.update({ + "kind": "decke", + "uk": uk, + "ok": ok, + }) + elif meta["type"] == "dach_outline": + base_h = _resolve_dach_base(doc, meta["geschoss"], meta["uk_override"]) + base.update({ + "kind": "dach", + "uk": base_h, + "ok": base_h, + "neigung": meta.get("neigung", 30.0), + "eaveIdx": meta.get("eave_idx", 0), + "dachTyp": meta.get("dach_typ", "pult"), + "neigungUnten": meta.get("neigung_unten", 60.0), + "knickH": meta.get("knick_h", 2.0), + "dachVariante": meta.get("dach_variante", "walm"), + }) + elif meta["type"] == "oeffnung_point": + base.update({ + "kind": meta.get("oeff_typ", "fenster"), + "parentId": meta.get("oeff_parent", ""), + "breite": meta.get("oeff_breite", 1.0), + "hoehe": meta.get("oeff_hoehe", 1.4), + "brueest": meta.get("oeff_brueest", 0.9), + "rahmenB": meta.get("oeff_rahmen_b", 0.06), + "rahmenTiefe": meta.get("oeff_rahmen_tiefe", 0.08), + "rahmenPos": meta.get("oeff_rahmen_pos", "mid"), + "fluegel": meta.get("oeff_fluegel", 1), + "simsAus": meta.get("oeff_sims_aus", "ohne"), + "simsIn": meta.get("oeff_sims_in", "ohne"), + "glas": bool(meta.get("oeff_glas", False)), + "oeffReferenz": meta.get("oeff_referenz", "mid"), + }) + else: # treppe_axis + gs = _geschoss_by_id(doc, meta["geschoss"]) + ge = _geschoss_by_id(doc, meta.get("geschoss_end", "")) + try: uk = float(gs.get("okff", 0.0)) if gs else 0.0 + except Exception: uk = 0.0 + hov = meta.get("treppe_h_over", "") + if hov: + try: ok = uk + float(hov) + except Exception: ok = uk + 3.0 + elif ge is not None: + try: ok = float(ge.get("okff", uk + 3.0)) + except Exception: ok = uk + 3.0 + else: + try: ok = uk + float(gs.get("hoehe", 3.0)) if gs else uk + 3.0 + except Exception: ok = uk + 3.0 + # Lauflinien-Laenge aus dem Source-Curve + try: lauf_len = float(obj.Geometry.GetLength()) + except Exception: lauf_len = 0.0 + base.update({ + "kind": "treppe", + "geschossEnd": meta.get("geschoss_end", ""), + "geschossEndName": (ge.get("name") if ge else ""), + "breite": meta.get("treppe_breite", 1.0), + "nStufen": meta.get("treppe_n", 15), + "treppeReferenz": meta.get("treppe_referenz", "mid"), + "treppeModus": meta.get("treppe_modus", "flach"), + "treppeArt": meta.get("treppe_art", "gerade"), + "laufD": meta.get("treppe_lauf_d", 0.18), + "laufLen": lauf_len, + "uk": uk, + "ok": ok, + "hOver": meta.get("treppe_h_over", ""), + "soll": meta.get("treppe_soll", _TREPPE_SOLL_DEFAULT), + }) + elements.append(base) + sel_id = next((e["id"] for e in elements if e["selected"]), None) + self.send("STATE", { + "elements": elements, + "geschosse": [{"id": g.get("id"), "name": g.get("name")} + for g in geschosse if isinstance(g, dict)], + "selection": sel_id, + "activeGeschoss": _active_geschoss_id(doc), + "activeGeschossName": _active_geschoss_name(doc), + }) + + # --- Wand-Befehle ------------------------------------------------------- + + def _cmd_create_wall(self, p): + """Interaktive Wand-Erzeugung mit Modus-Auswahl. + Modi: + - Polylinie: mehrere Punkte, Enter / Klick auf letzten = fertig + - Linie: 2 Punkte, danach automatisch fertig + - Spline: mehrere Kontrollpunkte, Enter = fertig, Achse wird ein + interpolierter NURBS + - Bogen: 3-Punkt-Bogen (Anfang, Mittelpunkt, Ende) + Plus Optionen: Referenz, Dicke.""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + geschoss = p.get("geschoss") or _active_geschoss_id(doc) + if not geschoss: + print("[ELEMENTE] Kein Geschoss aktiv"); return + # Last-Used: Werte ueberleben den Befehl. Frontend kann ueberschreiben, + # sonst nehmen wir den letzten Wert aus der Session. + d_in = p.get("dicke") + try: dicke = float(d_in) if d_in else _last("wand_dicke", 0.25) + except Exception: dicke = _last("wand_dicke", 0.25) + uk_over = p.get("ukOverride", "") + ok_over = p.get("okOverride", "") + referenz = p.get("referenz") or _last("wand_referenz", "mid") + + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception as ex: + print("[ELEMENTE] Imports:", ex); return + + modi = ["Polylinie", "Linie", "Rechteck", "Spline", "Bogen"] + modus = p.get("modus") or _last("wand_modus", "Polylinie") + if modus not in modi: modus = "Polylinie" + ref_codes = ["mid", "left", "right"] + ref_labels = ["Mittig", "Links", "Rechts"] + try: ref_idx = ref_codes.index(referenz) + except ValueError: ref_idx = 0 + + def _build_prompt(base): + return "{} [Modus={}, Referenz={}, Dicke={:.3f}]".format( + base, modus, ref_labels[ref_idx], dicke) + + first_pt = None + try: + while True: + gp = ric.GetPoint() + gp.SetCommandPrompt(_build_prompt("Wand: Startpunkt")) + opt_modus = gp.AddOptionList("Modus", modi, modi.index(modus)) + opt_ref = gp.AddOptionList("Referenz", ref_labels, ref_idx) + opt_dicke = gp.AddOption("Dicke") + res = gp.Get() + if res == GetResult.Option: + if gp.OptionIndex() == opt_modus: + try: modus = modi[gp.Option().CurrentListOptionIndex] + except Exception: pass + elif gp.OptionIndex() == opt_ref: + try: ref_idx = gp.Option().CurrentListOptionIndex + except Exception: pass + elif gp.OptionIndex() == opt_dicke: + try: + gn = ric.GetNumber() + gn.SetCommandPrompt("Wand-Dicke") + gn.SetDefaultNumber(dicke) + gn.SetLowerLimit(0.01, False) + if gn.Get() == GetResult.Number: + dicke = float(gn.Number()) + except Exception as ex: print("[ELEMENTE] GetNumber:", ex) + continue + if res != GetResult.Point: return + first_pt = gp.Point() + break + except Exception as ex: + print("[ELEMENTE] wand first-point:", ex); return + + referenz = ref_codes[ref_idx] + try: + if modus == "Rechteck": + axes = self._collect_wall_rectangle(doc, first_pt, dicke, referenz) + if not axes: + print("[ELEMENTE] Rechteck abgebrochen"); return + for ac in axes: + self._make_wall_from_axis(doc, ac, geschoss, dicke, + uk_over, ok_over, referenz) + _save_last(wand_dicke=dicke, wand_referenz=referenz, wand_modus=modus) + self._send_state() + return + if modus == "Polylinie": + axis_curve = self._collect_wall_polyline(doc, first_pt, dicke, + referenz, ends_after=None) + elif modus == "Linie": + axis_curve = self._collect_wall_polyline(doc, first_pt, dicke, + referenz, ends_after=1) + elif modus == "Spline": + axis_curve = self._collect_wall_spline(doc, first_pt, dicke, referenz) + elif modus == "Bogen": + axis_curve = self._collect_wall_arc(doc, first_pt, dicke, referenz) + else: + axis_curve = None + except Exception as ex: + print("[ELEMENTE] wand collect:", ex); return + + if axis_curve is None: + print("[ELEMENTE] keine gueltige Achse"); return + self._make_wall_from_axis(doc, axis_curve, geschoss, dicke, + uk_over, ok_over, referenz) + _save_last(wand_dicke=dicke, wand_referenz=referenz, wand_modus=modus) + self._send_state() + + def _collect_wall_polyline(self, doc, first_pt, dicke, referenz, ends_after=None): + """Sammelt eine Polyline-Achse. ends_after=N: nach N weiteren + Punkten automatisch beenden (1 = klassische Linie aus 2 Punkten).""" + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception: return None + points = [first_pt] + tol = max(doc.ModelAbsoluteTolerance, 1e-4) + while True: + if ends_after is not None and len(points) > ends_after: + break + gp = ric.GetPoint() + label = "Endpunkt" if ends_after == 1 else \ + "Naechster Punkt (Enter = fertig)" + gp.SetCommandPrompt(label) + if ends_after != 1: + gp.AcceptNothing(True) + try: gp.SetBasePoint(points[-1], True) + except Exception: pass + try: + preview = _make_preview_handler(list(points), dicke, referenz) + gp.DynamicDraw += preview + except Exception: pass + res = gp.Get() + if res == GetResult.Nothing: break + if res != GetResult.Point: break + pt = gp.Point() + if pt.DistanceTo(points[-1]) < tol and ends_after != 1: + break + points.append(pt) + if len(points) < 2: return None + pts3d = [rg.Point3d(p.X, p.Y, 0) for p in points] + if len(pts3d) == 2: + return rg.LineCurve(pts3d[0], pts3d[1]) + return rg.PolylineCurve(rg.Polyline(pts3d)) + + def _collect_wall_spline(self, doc, first_pt, dicke, referenz): + """Spline-Wand: interpolierter NURBS durch Kontrollpunkte. Live-Preview + zeigt Kurve + Wand-Kanten.""" + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception: return None + points = [first_pt] + tol = max(doc.ModelAbsoluteTolerance, 1e-4) + while True: + gp = ric.GetPoint() + gp.SetCommandPrompt("Spline-Kontrollpunkt (Enter = fertig)") + gp.AcceptNothing(True) + try: gp.SetBasePoint(points[-1], True) + except Exception: pass + try: + preview = _make_spline_preview_handler(list(points), dicke, referenz) + gp.DynamicDraw += preview + except Exception: pass + res = gp.Get() + if res == GetResult.Nothing: break + if res != GetResult.Point: break + pt = gp.Point() + if pt.DistanceTo(points[-1]) < tol: break + points.append(pt) + if len(points) < 2: return None + pts3d = [rg.Point3d(p.X, p.Y, 0) for p in points] + if len(pts3d) == 2: + return rg.LineCurve(pts3d[0], pts3d[1]) + try: + return rg.Curve.CreateInterpolatedCurve(pts3d, 3) + except Exception: + return rg.PolylineCurve(rg.Polyline(pts3d)) + + def _collect_wall_arc(self, doc, first_pt, dicke, referenz): + """3-Punkt-Bogen mit Live-Preview ueber 2 Phasen (Mittel- und Endpunkt).""" + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception: return None + # Phase 1: Punkt auf dem Bogen + gp = ric.GetPoint() + gp.SetCommandPrompt("Punkt auf dem Bogen") + try: gp.SetBasePoint(first_pt, True) + except Exception: pass + try: + preview = _make_arc_preview_handler(first_pt, None, dicke, referenz) + gp.DynamicDraw += preview + except Exception: pass + res = gp.Get() + if res != GetResult.Point: return None + mid = gp.Point() + # Phase 2: Endpunkt — mit Bogen-Preview + gp = ric.GetPoint() + gp.SetCommandPrompt("Endpunkt") + try: gp.SetBasePoint(mid, True) + except Exception: pass + try: + preview = _make_arc_preview_handler(first_pt, mid, dicke, referenz) + gp.DynamicDraw += preview + except Exception: pass + res = gp.Get() + if res != GetResult.Point: return None + end = gp.Point() + p0 = rg.Point3d(first_pt.X, first_pt.Y, 0) + p1 = rg.Point3d(mid.X, mid.Y, 0) + p2 = rg.Point3d(end.X, end.Y, 0) + arc = rg.Arc(p0, p1, p2) + if not arc.IsValid: return None + return rg.ArcCurve(arc) + + def _collect_wall_rectangle(self, doc, c1, dicke, referenz): + """Rechteck-Wand: zweite (diagonale) Ecke. Liefert Liste von 4 + Line-Curves (eine pro Seite) — vier eigenstaendige Waende.""" + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception: return None + gp = ric.GetPoint() + gp.SetCommandPrompt("Gegenueberliegende Ecke") + try: gp.SetBasePoint(c1, True) + except Exception: pass + try: + preview = _make_rectangle_wall_preview_handler(c1, dicke, referenz) + gp.DynamicDraw += preview + except Exception: pass + res = gp.Get() + if res != GetResult.Point: return None + c2 = gp.Point() + p1 = rg.Point3d(c1.X, c1.Y, 0) + p2 = rg.Point3d(c2.X, c1.Y, 0) + p3 = rg.Point3d(c2.X, c2.Y, 0) + p4 = rg.Point3d(c1.X, c2.Y, 0) + # Im Uhrzeigersinn — Referenz "links" landet damit innen, "rechts" aussen + return [ + rg.LineCurve(p1, p2), + rg.LineCurve(p2, p3), + rg.LineCurve(p3, p4), + rg.LineCurve(p4, p1), + ] + + def _make_wall_from_axis(self, doc, axis_curve, geschoss_id, dicke, + uk_over, ok_over, referenz): + """Erzeugt Wand aus beliebiger Achsen-Curve (Line, Polyline, NURBS, Arc).""" + wall_id = "wall_" + uuid.uuid4().hex[:10] + g = _geschoss_by_id(doc, geschoss_id) + geschoss_name = g.get("name", "EG") if g else "EG" + axis_layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) + axis = axis_curve.DuplicateCurve() + try: + z0 = axis.PointAtStart.Z + if abs(z0) > 1e-6: + axis.Transform(rg.Transform.Translation(0, 0, -z0)) + except Exception: pass + attrs = Rhino.DocObjects.ObjectAttributes() + attrs.LayerIndex = axis_layer + _attach_meta(attrs, wall_id, "wand_axis", geschoss_id, + dicke, uk_over, ok_over, referenz) + if doc.Objects.AddCurve(axis, attrs) == System.Guid.Empty: + print("[ELEMENTE] Wand AddCurve fehlgeschlagen"); return + _regenerate_element(doc, wall_id) + doc.Views.Redraw() + print("[ELEMENTE] Wand erzeugt: {}".format(wall_id)) + + def _cmd_create_decke(self, p): + """Decken-Erzeugung mit Modus-Auswahl: Polylinie, Rechteck, + Rechteck-3-Punkte oder Kreis. Modus + Dicke per Command-Option.""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + geschoss = p.get("geschoss") or _active_geschoss_id(doc) + if not geschoss: + print("[ELEMENTE] Kein Geschoss aktiv"); return + d_in = p.get("dicke") + try: dicke = float(d_in) if d_in else _last("decke_dicke", 0.20) + except Exception: dicke = _last("decke_dicke", 0.20) + uk_over = p.get("ukOverride", "") + ok_over = p.get("okOverride", "") + + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception as ex: + print("[ELEMENTE] Imports:", ex); return + + modi = ["Polylinie", "Rechteck", "Rechteck3Punkte", "Kreis"] + modus = p.get("modus") or _last("decke_modus", "Polylinie") + if modus not in modi: modus = "Polylinie" + + def _build_prompt(base): + return "{} [Modus={}, Dicke={:.3f}]".format(base, modus, dicke) + + first_pt = None + try: + # Erster Punkt + Optionen + while True: + gp = ric.GetPoint() + gp.SetCommandPrompt(_build_prompt("Decke: Startpunkt")) + opt_modus = gp.AddOptionList("Modus", modi, modi.index(modus)) + opt_dicke = gp.AddOption("Dicke") + res = gp.Get() + if res == GetResult.Option: + if gp.OptionIndex() == opt_modus: + try: modus = modi[gp.Option().CurrentListOptionIndex] + except Exception: pass + elif gp.OptionIndex() == opt_dicke: + try: + gn = ric.GetNumber() + gn.SetCommandPrompt("Decken-Dicke") + gn.SetDefaultNumber(dicke) + gn.SetLowerLimit(0.01, False) + if gn.Get() == GetResult.Number: + dicke = float(gn.Number()) + except Exception as ex: print("[ELEMENTE] GetNumber:", ex) + continue + if res != GetResult.Point: return + first_pt = gp.Point() + break + except Exception as ex: + print("[ELEMENTE] decke first-point:", ex); return + + outline_curve = None + try: + if modus == "Polylinie": + outline_curve = self._collect_polyline_outline(doc, first_pt) + elif modus == "Rechteck": + outline_curve = _collect_rectangle(doc, first_pt) + elif modus == "Rechteck3Punkte": + outline_curve = _collect_rectangle_3pt(doc, first_pt) + elif modus == "Kreis": + outline_curve = _collect_circle(doc, first_pt) + except Exception as ex: + print("[ELEMENTE] decke collect:", ex) + return + + if outline_curve is None or not outline_curve.IsClosed: + print("[ELEMENTE] keine gueltige Outline") + return + self._make_decke_from_outline(doc, outline_curve, geschoss, dicke, + uk_over, ok_over) + _save_last(decke_dicke=dicke, decke_modus=modus) + self._send_state() + + def _collect_polyline_outline(self, doc, first_pt): + """Sammelt eine geschlossene Polyline via aufeinanderfolgendes + GetPoint mit Live-Preview. Enter / Klick auf Startpunkt schliesst.""" + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception: return None + points = [first_pt] + tol = max(doc.ModelAbsoluteTolerance, 1e-4) + while True: + gp = ric.GetPoint() + gp.SetCommandPrompt("Naechster Punkt (Enter / Klick auf Start = schliessen)") + gp.AcceptNothing(True) + try: gp.SetBasePoint(points[-1], True) + except Exception: pass + try: + preview = _make_decke_preview_handler(list(points)) + gp.DynamicDraw += preview + except Exception: pass + res = gp.Get() + if res == GetResult.Nothing: break + if res != GetResult.Point: break + pt = gp.Point() + if pt.DistanceTo(points[0]) < tol: break + if pt.DistanceTo(points[-1]) < tol: break + points.append(pt) + if len(points) < 3: return None + pts3d = [rg.Point3d(p.X, p.Y, 0) for p in points] + if pts3d[0].DistanceTo(pts3d[-1]) > 1e-6: + pts3d.append(pts3d[0]) + return rg.PolylineCurve(rg.Polyline(pts3d)) + + def _make_decke_from_outline(self, doc, outline_curve, geschoss_id, dicke, + uk_over, ok_over): + """Decke aus beliebiger geschlossener Outline-Curve (Polyline, Rechteck, + Kreis, ...). Curve wird so wie sie ist gespeichert; das Volumen wird + per Extrusion erzeugt.""" + element_id = "decke_" + uuid.uuid4().hex[:10] + g = _geschoss_by_id(doc, geschoss_id) + geschoss_name = g.get("name", "EG") if g else "EG" + layer = _ensure_layer(doc, _layer_path_decke(doc, geschoss_name)) + # Sicherstellen dass die Curve auf Z=0 liegt + outline = outline_curve.DuplicateCurve() + try: + z0 = outline.PointAtStart.Z + if abs(z0) > 1e-6: + outline.Transform(rg.Transform.Translation(0, 0, -z0)) + except Exception: pass + attrs = Rhino.DocObjects.ObjectAttributes() + attrs.LayerIndex = layer + _attach_meta(attrs, element_id, "decke_outline", geschoss_id, + dicke, uk_over, ok_over, "mid") + outline_id = doc.Objects.AddCurve(outline, attrs) + if outline_id == System.Guid.Empty: + print("[ELEMENTE] Decke AddCurve fehlgeschlagen"); return + _regenerate_element(doc, element_id) + doc.Views.Redraw() + print("[ELEMENTE] Decke erzeugt: {}".format(element_id)) + + def _cmd_create_dach(self, p): + """Pultdach-Erzeugung mit Modus-Auswahl: Polylinie / Rechteck / + Rechteck-3-Punkte. Die ERSTE Kante (Punkt 1 → Punkt 2) ist die + Traufkante. Neigung + Dicke per Command-Option.""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + geschoss = p.get("geschoss") or _active_geschoss_id(doc) + if not geschoss: + print("[ELEMENTE] Kein Geschoss aktiv"); return + d_in = p.get("dicke") + try: dicke = float(d_in) if d_in else _last("dach_dicke", 0.20) + except Exception: dicke = _last("dach_dicke", 0.20) + n_in = p.get("neigung") + try: neigung = float(n_in) if n_in else _last("dach_neigung", 30.0) + except Exception: neigung = _last("dach_neigung", 30.0) + uk_over = p.get("ukOverride", "") + + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception as ex: + print("[ELEMENTE] Imports:", ex); return + + modi = ["Polylinie", "Rechteck", "Rechteck3Punkte"] + modus = p.get("modus") or _last("dach_modus", "Polylinie") + if modus not in modi: modus = "Polylinie" + + typen = ["Pult", "Sattel", "Walm", "Mansarde"] + dach_typ = p.get("dachTyp") or _last("dach_typ", "Pult") + if dach_typ not in typen: dach_typ = "Pult" + # Mansarde-spezifische Defaults + try: neigung_unten = float(p.get("neigungUnten") or _last("dach_neigung_unten", 60.0)) + except Exception: neigung_unten = 60.0 + try: knick_h = float(p.get("knickH") or _last("dach_knick_h", 2.0)) + except Exception: knick_h = 2.0 + varianten = ["Walm", "Giebel", "Walm-Giebel"] + variante_code_map = {"Walm": "walm", "Giebel": "giebel", + "Walm-Giebel": "walm_giebel"} + dach_variante = p.get("dachVariante") or _last("dach_variante", "Walm") + if dach_variante not in varianten: dach_variante = "Walm" + + def _build_prompt(base): + extra = "" + if dach_typ == "Mansarde": + extra = ", Variante={}".format(dach_variante) + return "{} [Typ={}{}, Modus={}, Neigung={:.1f}°, Dicke={:.3f}]".format( + base, dach_typ, extra, modus, neigung, dicke) + + first_pt = None + try: + while True: + gp = ric.GetPoint() + gp.SetCommandPrompt(_build_prompt("Dach: Startpunkt (1. Kante = Traufe)")) + opt_typ = gp.AddOptionList("Typ", typen, typen.index(dach_typ)) + opt_var = None + if dach_typ == "Mansarde": + opt_var = gp.AddOptionList("Variante", varianten, + varianten.index(dach_variante)) + opt_modus = gp.AddOptionList("Modus", modi, modi.index(modus)) + opt_n = gp.AddOption("Neigung") + opt_d = gp.AddOption("Dicke") + res = gp.Get() + if res == GetResult.Option: + if gp.OptionIndex() == opt_typ: + try: dach_typ = typen[gp.Option().CurrentListOptionIndex] + except Exception: pass + elif opt_var is not None and gp.OptionIndex() == opt_var: + try: dach_variante = varianten[gp.Option().CurrentListOptionIndex] + except Exception: pass + elif gp.OptionIndex() == opt_modus: + try: modus = modi[gp.Option().CurrentListOptionIndex] + except Exception: pass + elif gp.OptionIndex() == opt_n: + try: + gn = ric.GetNumber() + gn.SetCommandPrompt("Dach-Neigung in Grad") + gn.SetDefaultNumber(neigung) + gn.SetLowerLimit(0.0, False) + gn.SetUpperLimit(89.0, False) + if gn.Get() == GetResult.Number: + neigung = float(gn.Number()) + except Exception as ex: print("[ELEMENTE] GetNumber:", ex) + elif gp.OptionIndex() == opt_d: + try: + gn = ric.GetNumber() + gn.SetCommandPrompt("Dach-Dicke") + gn.SetDefaultNumber(dicke) + gn.SetLowerLimit(0.01, False) + if gn.Get() == GetResult.Number: + dicke = float(gn.Number()) + except Exception as ex: print("[ELEMENTE] GetNumber:", ex) + continue + if res != GetResult.Point: return + first_pt = gp.Point() + break + except Exception as ex: + print("[ELEMENTE] dach first-point:", ex); return + + outline_curve = None + try: + if modus == "Polylinie": + outline_curve = self._collect_polyline_outline(doc, first_pt) + elif modus == "Rechteck": + outline_curve = _collect_rectangle(doc, first_pt) + elif modus == "Rechteck3Punkte": + outline_curve = _collect_rectangle_3pt(doc, first_pt) + except Exception as ex: + print("[ELEMENTE] dach collect:", ex); return + + if outline_curve is None or not outline_curve.IsClosed: + print("[ELEMENTE] keine gueltige Outline"); return + # Sattel/Walm/Mansarde brauchen Rechteck-Outline. Sonst Fallback Pult. + dt_code = dach_typ.lower() + if dt_code in ("sattel", "walm", "mansarde"): + try: + ok, poly = outline_curve.TryGetPolyline() + if not ok or poly is None or poly.Count != 5: + print("[ELEMENTE] {} braucht Rechteck-Outline — Fallback Pult".format(dach_typ)) + dt_code = "pult" + except Exception: dt_code = "pult" + self._make_dach_from_outline(doc, outline_curve, geschoss, dicke, + neigung, 0, uk_over, dt_code, + neigung_unten=neigung_unten, + knick_h=knick_h, + dach_variante=variante_code_map.get( + dach_variante, "walm")) + _save_last(dach_dicke=dicke, dach_neigung=neigung, + dach_modus=modus, dach_typ=dach_typ, + dach_neigung_unten=neigung_unten, dach_knick_h=knick_h, + dach_variante=dach_variante) + self._send_state() + + def _make_dach_from_outline(self, doc, outline_curve, geschoss_id, dicke, + neigung, eave_idx, uk_over, dach_typ="pult", + neigung_unten=60.0, knick_h=2.0, + dach_variante="walm"): + """Dach aus geschlossener Outline-PolylineCurve.""" + element_id = "dach_" + uuid.uuid4().hex[:10] + g = _geschoss_by_id(doc, geschoss_id) + geschoss_name = g.get("name", "EG") if g else "EG" + layer = _ensure_layer(doc, _layer_path_dach(doc, geschoss_name)) + outline = outline_curve.DuplicateCurve() + try: + z0 = outline.PointAtStart.Z + if abs(z0) > 1e-6: + outline.Transform(rg.Transform.Translation(0, 0, -z0)) + except Exception: pass + attrs = Rhino.DocObjects.ObjectAttributes() + attrs.LayerIndex = layer + _attach_meta(attrs, element_id, "dach_outline", geschoss_id, + dicke, uk_over, "", "mid", + neigung=neigung, eave_idx=eave_idx, dach_typ=dach_typ, + neigung_unten=neigung_unten, knick_h=knick_h, + dach_variante=dach_variante) + outline_id = doc.Objects.AddCurve(outline, attrs) + if outline_id == System.Guid.Empty: + print("[ELEMENTE] Dach AddCurve fehlgeschlagen"); return + _regenerate_element(doc, element_id) + doc.Views.Redraw() + print("[ELEMENTE] Dach erzeugt: {} ({}, neigung={}°)".format( + element_id, dach_typ, neigung)) + + + def _cmd_create_oeffnung(self, p, typ): + """Fenster/Tuer-Erzeugung: User klickt auf eine Wand-Achse, dann + einen Punkt darauf. Punkt-Position wird auf die Achse projiziert. + Optionen: Breite, Hoehe, (Bruestung nur fuer Fenster).""" + if typ not in ("fenster", "tuer"): return + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception as ex: + print("[ELEMENTE] Imports:", ex); return + + # Defaults + if typ == "fenster": + b_def = _last("fenster_breite", 1.20) + h_def = _last("fenster_hoehe", 1.40) + br_def = _last("fenster_brueest", 0.90) + else: + b_def = _last("tuer_breite", 0.90) + h_def = _last("tuer_hoehe", 2.10) + br_def = 0.0 + try: breite = float(p.get("breite") or b_def) + except Exception: breite = b_def + try: hoehe = float(p.get("hoehe") or h_def) + except Exception: hoehe = h_def + if typ == "fenster": + try: brueest = float(p.get("brueest") or br_def) + except Exception: brueest = br_def + else: + brueest = 0.0 + + # 1) Wand-Achse waehlen + try: + gw = ric.GetObject() + gw.SetCommandPrompt("Wand-Achse fuer {} waehlen".format( + "Fenster" if typ == "fenster" else "Tuer")) + gw.GeometryFilter = Rhino.DocObjects.ObjectType.Curve + def _filter_wand(rhObj, geom, ci): + m = _read_meta(rhObj) + return m is not None and m.get("type") == "wand_axis" + gw.SetCustomGeometryFilter(_filter_wand) + res = gw.Get() + if res != GetResult.Object: return + wall_obj = gw.Object(0).Object() + except Exception as ex: + print("[ELEMENTE] Wand-Auswahl:", ex); return + wall_meta = _read_meta(wall_obj) + if wall_meta is None: return + axis_curve = wall_obj.Geometry + if not isinstance(axis_curve, rg.Curve): return + + # 2) Punkt auf der Achse — constrained an die Wand-Achse + try: + while True: + gp = ric.GetPoint() + prompt = "Position fuer {} [B={:.2f}, H={:.2f}".format( + "Fenster" if typ == "fenster" else "Tuer", breite, hoehe) + if typ == "fenster": + prompt += ", Br={:.2f}".format(brueest) + prompt += "]" + gp.SetCommandPrompt(prompt) + try: gp.Constrain(axis_curve, False) + except Exception: pass + opt_b = gp.AddOption("Breite") + opt_h = gp.AddOption("Hoehe") + opt_br = gp.AddOption("Bruestung") if typ == "fenster" else None + rp = gp.Get() + if rp == GetResult.Option: + idx = gp.OptionIndex() + if idx == opt_b: + gn = ric.GetNumber() + gn.SetCommandPrompt("Breite") + gn.SetDefaultNumber(breite) + gn.SetLowerLimit(0.05, False) + if gn.Get() == GetResult.Number: breite = float(gn.Number()) + elif idx == opt_h: + gn = ric.GetNumber() + gn.SetCommandPrompt("Hoehe") + gn.SetDefaultNumber(hoehe) + gn.SetLowerLimit(0.05, False) + if gn.Get() == GetResult.Number: hoehe = float(gn.Number()) + elif opt_br is not None and idx == opt_br: + gn = ric.GetNumber() + gn.SetCommandPrompt("Bruestungshoehe") + gn.SetDefaultNumber(brueest) + gn.SetLowerLimit(0.0, True) + if gn.Get() == GetResult.Number: brueest = float(gn.Number()) + continue + if rp != GetResult.Point: return + click_pt = gp.Point() + break + except Exception as ex: + print("[ELEMENTE] Oeffnung point:", ex); return + + # Auf Achse projizieren (Constrain garantiert das eigentlich schon) + try: + ok, t = axis_curve.ClosestPoint(click_pt) + if not ok: return + on_axis = axis_curve.PointAt(t) + except Exception as ex: + print("[ELEMENTE] ClosestPoint:", ex); return + + # Point-Objekt mit Metadaten anlegen + prefix = "fenster_" if typ == "fenster" else "tuer_" + oeff_id = prefix + uuid.uuid4().hex[:10] + geschoss = wall_meta["geschoss"] + g = _geschoss_by_id(doc, geschoss) + geschoss_name = g.get("name", "EG") if g else "EG" + layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) + + # Entwurfs-Defaults pro Typ + is_fenster = (typ == "fenster") + rahmen_b_def = _last("oeff_rahmen_b", 0.06) + rahmen_t_def = _last("oeff_rahmen_tiefe", 0.08) + rahmen_p_def = _last("oeff_rahmen_pos", "mid") + fluegel_def = _last("{}_fluegel".format(typ), 2 if is_fenster else 1) + simsa_def = "standard" if is_fenster else "ohne" + simsi_def = "standard" if is_fenster else "ohne" + glas_def = is_fenster + referenz_def = _last("oeff_referenz", "mid") + + attrs = Rhino.DocObjects.ObjectAttributes() + attrs.LayerIndex = layer + _attach_meta(attrs, oeff_id, "oeffnung_point", geschoss, + wall_meta["dicke"], "", "", + oeff_typ=typ, oeff_parent=wall_meta["id"], + oeff_breite=breite, oeff_hoehe=hoehe, + oeff_brueest=brueest, + oeff_rahmen_b=rahmen_b_def, + oeff_rahmen_tiefe=rahmen_t_def, + oeff_rahmen_pos=rahmen_p_def, + oeff_fluegel=fluegel_def, + oeff_sims_aus=simsa_def, + oeff_sims_in=simsi_def, + oeff_glas=glas_def, + oeff_referenz=referenz_def) + new_id = doc.Objects.AddPoint(on_axis, attrs) + if new_id == System.Guid.Empty: + print("[ELEMENTE] AddPoint fehlgeschlagen"); return + + # Last-used + if typ == "fenster": + _save_last(fenster_breite=breite, fenster_hoehe=hoehe, + fenster_brueest=brueest) + else: + _save_last(tuer_breite=breite, tuer_hoehe=hoehe) + + # Eltern-Wand regen + _regenerate_element(doc, wall_meta["id"]) + doc.Views.Redraw() + print("[ELEMENTE] {} erzeugt: {} an Wand {}".format( + "Fenster" if typ == "fenster" else "Tuer", oeff_id, wall_meta["id"])) + self._send_state() + + def _cmd_create_treppe(self, p): + """Treppen-Erzeugung. Hoehe automatisch aus Geschoss-OKFF-Differenz. + treppeArt aus Payload: 'gerade' (2 Punkte) | 'l' (3 Punkte mit Eck).""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + treppe_art = p.get("treppeArt") or "gerade" + if treppe_art not in _TREPPE_ARTEN: treppe_art = "gerade" + geschoss_start = p.get("geschoss") or _active_geschoss_id(doc) + if not geschoss_start: + print("[ELEMENTE] Kein Geschoss aktiv"); return + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception as ex: + print("[ELEMENTE] Imports:", ex); return + + # Zielgeschoss-Default: das naechste Geschoss vom gleichen Typ in der + # Liste, oder None falls keins existiert. + geschosse = _load_geschosse(doc) + gs = _geschoss_by_id(doc, geschoss_start) + if gs is None: + print("[ELEMENTE] Startgeschoss nicht gefunden"); return + geschoss_end = p.get("geschossEnd") or "" + if not geschoss_end: + # naechstes Geschoss > start_okff + try: + start_okff = float(gs.get("okff", 0.0)) + candidates = [] + for g in geschosse: + if not isinstance(g, dict): continue + if g.get("type") != "grundriss": continue + if g.get("id") == geschoss_start: continue + try: o = float(g.get("okff", 0.0)) + except Exception: continue + if o > start_okff + 1e-6: + candidates.append((o, g.get("id", ""))) + candidates.sort() + if candidates: + geschoss_end = candidates[0][1] + except Exception: pass + g_end = _geschoss_by_id(doc, geschoss_end) if geschoss_end else None + + # Defaults + try: breite = float(p.get("breite") or _last("treppe_breite", 1.0)) + except Exception: breite = 1.0 + referenz = p.get("referenz") or _last("treppe_referenz", "mid") + if referenz not in ("mid", "links", "rechts"): referenz = "mid" + # N: bei bekannter Hoehe ~S=0.18 berechnen + try: uk = float(gs.get("okff", 0.0)) + except Exception: uk = 0.0 + if g_end is not None: + try: ok = float(g_end.get("okff", uk + 3.0)) + except Exception: ok = uk + 3.0 + else: + try: ok = uk + float(gs.get("hoehe", 3.0)) + except Exception: ok = uk + 3.0 + H = max(0.001, ok - uk) + n_default = int(round(H / 0.18)) + if n_default < 2: n_default = 2 + try: n_stufen = int(p.get("nStufen") or _last("treppe_n", n_default)) + except Exception: n_stufen = n_default + if n_stufen < 2: n_stufen = 2 + + # Schrittmass-Regel: Default "regel" (Lauflinie wird auf optimale + # Laenge gezwungen). User kann auf "frei" stellen. + regel_mode = _last("treppe_regel", "regel") + if regel_mode not in ("frei", "regel"): regel_mode = "regel" + # Soll-Werte (Editable in der Treppe-Property-Card) aus sticky laden. + # Default falls noch nichts gesetzt: 0.15-0.20 / 0.21-0.35 / 0.60-0.65. + soll_last = _last("treppe_soll", None) + soll = dict(_TREPPE_SOLL_DEFAULT) + if soll_last: + try: + import json + parsed = json.loads(soll_last) if isinstance(soll_last, str) else soll_last + if isinstance(parsed, dict): + for k in ("s", "a", "sa"): + v = parsed.get(k) + if isinstance(v, list) and len(v) >= 3: + soll[k] = [float(v[0]), float(v[1]), bool(v[2])] + except Exception: pass + + # Lauflaengen-Range fuer aktuelle N & H aus enabled Soll-Werten: + # - 2S+A in [sa_lo, sa_hi] (enabled) → A in [sa_lo-2S, sa_hi-2S] + # - A in [a_lo, a_hi] (enabled) + # → A_range = Schnitt aller Constraints, L_range = N*A_range + # Optimaler Mittelwert: L_opt = 0.63*N - 2*H + def _l_range(n, h): + n = max(1, int(n)) + s = float(h) / n + a_lo, a_hi = 0.05, 2.0 # weit-offene Defaults + if soll["sa"][2]: # 2S+A enabled + a_lo = max(a_lo, soll["sa"][0] - 2 * s) + a_hi = min(a_hi, soll["sa"][1] - 2 * s) + if soll["a"][2]: # A enabled + a_lo = max(a_lo, soll["a"][0]) + a_hi = min(a_hi, soll["a"][1]) + if a_lo > a_hi: # widerspruechliche Constraints — gib Range zurueck zentriert + mid = (a_lo + a_hi) * 0.5 + a_lo = a_hi = mid + return (n * a_lo, n * a_hi) + def _l_optimal(n, h): + lo, hi = _l_range(n, h) + return max(0.3, (lo + hi) * 0.5) + + # Zwei Punkte fuer die Lauflinie einsammeln + first_pt = None + try: + while True: + gp = ric.GetPoint() + end_name = (g_end.get("name") if g_end else "(naechste Ebene)") + L_opt_show = _l_optimal(n_stufen, H) + gp.SetCommandPrompt( + "Treppe: Startpunkt [Hoehe {:.2f}, Stufen={}, Breite={:.2f}, Ref={}, Modus={}{}, Ziel={}]".format( + H, n_stufen, breite, referenz, regel_mode, + " (L_opt={:.2f})".format(L_opt_show) if regel_mode == "regel" else "", + end_name)) + opt_n = gp.AddOption("Stufen") + opt_b = gp.AddOption("Breite") + opt_ref = gp.AddOptionList("Referenz", + ["Links", "Mittig", "Rechts"], + {"links":0, "mid":1, "rechts":2}.get(referenz, 1)) + # Regel-Option nur fuer gerade Treppen (bei L sind 2 Segmente + # mit Podest dazwischen — die Regel laesst sich nicht trivial + # auf 2 Klicks aufteilen). + opt_reg = -1 + if treppe_art != "l": + opt_reg = gp.AddOptionList("Regel", + ["frei", "Schrittmass"], + 1 if regel_mode == "regel" else 0) + res = gp.Get() + if res == GetResult.Option: + idx = gp.OptionIndex() + if idx == opt_n: + gn = ric.GetInteger() + gn.SetCommandPrompt("Anzahl Stufen (Steigungen)") + gn.SetDefaultInteger(n_stufen) + gn.SetLowerLimit(2, False) + gn.SetUpperLimit(40, False) + if gn.Get() == GetResult.Number: n_stufen = int(gn.Number()) + elif idx == opt_b: + gn = ric.GetNumber() + gn.SetCommandPrompt("Treppen-Breite") + gn.SetDefaultNumber(breite) + gn.SetLowerLimit(0.3, False) + if gn.Get() == GetResult.Number: breite = float(gn.Number()) + elif idx == opt_ref: + try: + v = ["links", "mid", "rechts"][gp.Option().CurrentListOptionIndex] + referenz = v + except Exception: pass + elif opt_reg >= 0 and idx == opt_reg: + try: + v = ["frei", "regel"][gp.Option().CurrentListOptionIndex] + regel_mode = v + except Exception: pass + continue + if res != GetResult.Point: return + first_pt = gp.Point() + break + except Exception as ex: + print("[ELEMENTE] treppe first-pt:", ex); return + + # Zweiter Punkt mit DynamicDraw. Bei Regel-Modus: Maus gibt nur + # die RICHTUNG vor — die Lauflinien-Laenge wird auf den optimalen + # Wert fixiert (wie Rhinos Rotate-Befehl). Kein Kreis-Constraint + # — der wuerde blockieren wenn die Maus weit weg ist. + gp2 = ric.GetPoint() + L_opt = _l_optimal(n_stufen, H) + if treppe_art == "l": + # L-Treppe: 2. Klick ist der Podest-Eck. Live-Preview zeigt + # N1/N2 fuer die Mausposition. Regel-Modus aus (zu komplex). + gp2.SetCommandPrompt( + "L-Treppe: Eck-Punkt (Podest-Mitte) [Stufen={}, Breite={:.2f}]".format( + n_stufen, breite)) + gp2.SetBasePoint(first_pt, True) + gp2.DynamicDraw += _make_treppe_l_corner_preview( + first_pt, breite, referenz, n_stufen, H) + elif treppe_art == "wendel": + # Wendel: 1. Klick = Mittelpunkt, 2. Klick = Start (Radius + # + Startwinkel). Preview: Linie center→Maus + Kreis. + gp2.SetCommandPrompt( + "Wendeltreppe: Start der Lauflinie (definiert Radius) [Stufen={}, Breite={:.2f}]".format( + n_stufen, breite)) + gp2.SetBasePoint(first_pt, True) + gp2.DrawLineFromPoint(first_pt, True) + elif regel_mode == "regel": + L_min, L_max = _l_range(n_stufen, H) + same = abs(L_max - L_min) < 1e-4 + if same: + gp2.SetCommandPrompt( + "Treppe: Richtung (Lauflaenge {:.2f} m, Schrittmass-Regel)".format(L_min)) + else: + gp2.SetCommandPrompt( + "Treppe: Endpunkt (Lauflaenge {:.2f}–{:.2f} m, Schrittmass-Regel)".format( + L_min, L_max)) + gp2.SetBasePoint(first_pt, True) + if same: + gp2.DynamicDraw += _make_treppe_preview_handler( + first_pt, breite, referenz, n_stufen, fixed_length=L_min) + else: + gp2.DynamicDraw += _make_treppe_preview_handler( + first_pt, breite, referenz, n_stufen, + min_length=L_min, max_length=L_max) + else: + gp2.SetCommandPrompt( + "Treppe: Endpunkt der Lauflinie (frei) [Stufen={}, Breite={:.2f}, Ref={}]".format( + n_stufen, breite, referenz)) + gp2.SetBasePoint(first_pt, True) + gp2.DynamicDraw += _make_treppe_preview_handler( + first_pt, breite, referenz, n_stufen) + if gp2.Get() != GetResult.Point: return + clicked = gp2.Point() + if regel_mode == "regel" and treppe_art == "gerade": + dx = clicked.X - first_pt.X + dy = clicked.Y - first_pt.Y + dist = (dx * dx + dy * dy) ** 0.5 + if dist < 1e-4: + print("[ELEMENTE] Keine Richtung gewaehlt"); return + L_min2, L_max2 = _l_range(n_stufen, H) + # Clamp Mauspos-Distanz in die Range (oder reskaliere auf fix + # wenn Range gleich null). + if abs(L_max2 - L_min2) < 1e-4: + final_L = L_min2 + else: + final_L = max(L_min2, min(L_max2, dist)) + second_pt = rg.Point3d(first_pt.X + dx / dist * final_L, + first_pt.Y + dy / dist * final_L, + first_pt.Z) + else: + second_pt = clicked + + # L-Treppe: dritter Punkt einsammeln (Endpunkt nach dem Eck) + if treppe_art == "l": + gp3 = ric.GetPoint() + gp3.SetCommandPrompt( + "L-Treppe: Endpunkt nach dem Podest [Stufen={}, Breite={:.2f}]".format( + n_stufen, breite)) + gp3.SetBasePoint(second_pt, True) + gp3.DynamicDraw += _make_treppe_preview_handler( + second_pt, breite, referenz, max(1, n_stufen // 2)) + if gp3.Get() != GetResult.Point: return + third_pt = gp3.Point() + p_first = rg.Point3d(first_pt.X, first_pt.Y, 0) + p_corner = rg.Point3d(second_pt.X, second_pt.Y, 0) + p_end = rg.Point3d(third_pt.X, third_pt.Y, 0) + pl = rg.Polyline([p_first, p_corner, p_end]) + line = rg.PolylineCurve(pl) + if line.GetLength() < 0.2: + print("[ELEMENTE] L-Lauflinie zu kurz"); return + elif treppe_art == "wendel": + # Wendel: 3. Klick = Endpunkt (Sweep-Winkel + Drehrichtung). + # Im Regel-Modus wird der Sweep auf den durch r_lauf + Soll + # zulaessigen Bereich geclampt. + gp3 = ric.GetPoint() + gp3.SetCommandPrompt( + "Wendeltreppe: Endpunkt der Lauflinie (definiert Drehwinkel) [Stufen={}, Modus={}]".format( + n_stufen, regel_mode)) + gp3.SetBasePoint(first_pt, True) + gp3.DynamicDraw += _make_treppe_wendel_preview( + first_pt, second_pt, breite, referenz, n_stufen, + total_h=H, soll=soll, regel_mode=regel_mode) + if gp3.Get() != GetResult.Point: return + third_pt = gp3.Point() + p_center = rg.Point3d(first_pt.X, first_pt.Y, 0) + p_start_w = rg.Point3d(second_pt.X, second_pt.Y, 0) + p_end_w = rg.Point3d(third_pt.X, third_pt.Y, 0) + + # Sweep + (im Regel-Modus) clampen auf gueltigen Bereich. + # Dann den Endpunkt entsprechend reskalieren. + import math + a_s_w, dlt_w = _wendel_sweep(p_center, p_start_w, p_end_w) + if regel_mode == "regel": + r_lauf = math.sqrt( + (p_start_w.X - p_center.X) ** 2 + + (p_start_w.Y - p_center.Y) ** 2) + try: + s_lo, s_hi = _wendel_sweep_range( + r_lauf, breite, referenz, n_stufen, H, soll) + except Exception: + s_lo, s_hi = 0.05, 2.0 * math.pi + raw = abs(dlt_w) + if raw < s_lo: clamped = s_lo + elif raw > s_hi: clamped = s_hi + else: clamped = raw + dlt_clamped = clamped * (1.0 if dlt_w >= 0 else -1.0) + # Neuen Endpunkt auf Kreis r_lauf bei Winkel a_s_w + dlt_clamped + a_final = a_s_w + dlt_clamped + p_end_w = rg.Point3d( + p_center.X + r_lauf * math.cos(a_final), + p_center.Y + r_lauf * math.sin(a_final), 0) + # Wichtig: dlt_w fuer den nachfolgenden < 0.05 Check aktualisieren + dlt_w = dlt_clamped + if abs(dlt_w) < 0.05: + print("[ELEMENTE] Wendel-Sweep zu klein"); return + pl = rg.Polyline([p_center, p_start_w, p_end_w]) + line = rg.PolylineCurve(pl) + else: + line = rg.LineCurve(rg.Point3d(first_pt.X, first_pt.Y, 0), + rg.Point3d(second_pt.X, second_pt.Y, 0)) + if line.GetLength() < 0.1: + print("[ELEMENTE] Lauflinie zu kurz"); return + + # Element anlegen + treppe_id = "treppe_" + uuid.uuid4().hex[:10] + geschoss_name = gs.get("name", "EG") + layer = _ensure_layer(doc, _layer_path_treppe(doc, geschoss_name)) + attrs = Rhino.DocObjects.ObjectAttributes() + attrs.LayerIndex = layer + modus_def = _last("treppe_modus", "flach") + if modus_def not in _TREPPE_MODI: modus_def = "flach" + try: lauf_d_def = float(_last("treppe_lauf_d", 0.18)) + except Exception: lauf_d_def = 0.18 + _attach_meta(attrs, treppe_id, "treppe_axis", geschoss_start, + breite, "", "", "mid", + geschoss_end=geschoss_end, + treppe_breite=breite, + treppe_n=n_stufen, + treppe_referenz=referenz, + treppe_modus=modus_def, + treppe_lauf_d=lauf_d_def, + treppe_art=treppe_art) + new_id = doc.Objects.AddCurve(line, attrs) + if new_id == System.Guid.Empty: + print("[ELEMENTE] AddCurve fehlgeschlagen"); return + save_kwargs = dict(treppe_breite=breite, treppe_n=n_stufen, + treppe_referenz=referenz, + treppe_modus=modus_def, + treppe_lauf_d=lauf_d_def, + treppe_art=treppe_art) + # regel_mode fuer gerade + wendel speichern (L hat keinen + # sinnvollen Regel-Modus — wuerde sonst die User-Praeferenz + # auf "frei" zuruecksetzen). + if treppe_art in ("gerade", "wendel"): + save_kwargs["treppe_regel"] = regel_mode + _save_last(**save_kwargs) + _regenerate_element(doc, treppe_id) + doc.Views.Redraw() + print("[ELEMENTE] Treppe erzeugt: {}".format(treppe_id)) + self._send_state() + + def _update_wall(self, p): + """Properties eines Elements aendern (Wand/Decke/Dach/Oeffnung). + Volumen wird anschliessend regeneriert.""" + doc = Rhino.RhinoDoc.ActiveDoc + wall_id = p.get("id") + if not wall_id: return + axis_obj, old_meta = _find_source(doc, wall_id) + if axis_obj is None or old_meta is None: return + # Treppe: Breite/Anzahl Stufen/Referenz/Zielgeschoss + if old_meta["type"] == "treppe_axis": + try: tb = float(p.get("breite", old_meta.get("treppe_breite", 1.0))) + except Exception: tb = old_meta.get("treppe_breite", 1.0) + try: tn = int(p.get("nStufen", old_meta.get("treppe_n", 15))) + except Exception: tn = old_meta.get("treppe_n", 15) + if tn < 2: tn = 2 + tref = p.get("treppeReferenz", old_meta.get("treppe_referenz", "mid")) + if tref not in ("mid", "links", "rechts"): tref = "mid" + tmod = p.get("treppeModus", old_meta.get("treppe_modus", "flach")) + if tmod not in _TREPPE_MODI: tmod = "flach" + try: tld = float(p.get("laufD", old_meta.get("treppe_lauf_d", 0.18))) + except Exception: tld = old_meta.get("treppe_lauf_d", 0.18) + gend = p.get("geschossEnd", old_meta.get("geschoss_end", "")) + gstart = p.get("geschoss", old_meta["geschoss"]) + attrs = axis_obj.Attributes + if gstart != old_meta["geschoss"]: + gs = _geschoss_by_id(doc, gstart) + gn = gs.get("name", "EG") if gs else "EG" + attrs.LayerIndex = _ensure_layer(doc, _layer_path_treppe(doc, gn)) + # Custom H + Soll-Werte + h_over = p.get("hOver", old_meta.get("treppe_h_over", "")) + if h_over is None: h_over = "" + if isinstance(h_over, (int, float)): h_over = "{:.4f}".format(float(h_over)) + soll_in = p.get("soll", old_meta.get("treppe_soll", _TREPPE_SOLL_DEFAULT)) + # Auf Form normieren + if not isinstance(soll_in, dict): soll_in = dict(_TREPPE_SOLL_DEFAULT) + soll_norm = {} + for k, dv in _TREPPE_SOLL_DEFAULT.items(): + v = soll_in.get(k, dv) + if isinstance(v, list) and len(v) >= 3: + try: soll_norm[k] = [float(v[0]), float(v[1]), bool(v[2])] + except Exception: soll_norm[k] = list(dv) + else: soll_norm[k] = list(dv) + _attach_meta(attrs, wall_id, "treppe_axis", + gstart, tb, "", "", "mid", + geschoss_end=gend, + treppe_breite=tb, + treppe_n=tn, + treppe_referenz=tref, + treppe_modus=tmod, + treppe_lauf_d=tld, + treppe_art=old_meta.get("treppe_art", "gerade"), + treppe_h_over=h_over, + treppe_soll=soll_norm) + # Persistenz fuer Creation Default + try: + import json + _save_last(treppe_soll=json.dumps(soll_norm)) + except Exception: pass + axis_obj.Attributes = attrs + axis_obj.CommitChanges() + _regenerate_volume(doc, wall_id) + doc.Views.Redraw() + self._send_state() + return + # Oeffnung: Breite/Hoehe/Bruestung + Rahmen/Fluegel/Sims/Glas + if old_meta["type"] == "oeffnung_point": + try: breite = float(p.get("breite", old_meta.get("oeff_breite", 1.0))) + except Exception: breite = old_meta.get("oeff_breite", 1.0) + try: hoehe = float(p.get("hoehe", old_meta.get("oeff_hoehe", 1.4))) + except Exception: hoehe = old_meta.get("oeff_hoehe", 1.4) + otyp = old_meta.get("oeff_typ", "fenster") + if otyp == "fenster": + try: brueest = float(p.get("brueest", old_meta.get("oeff_brueest", 0.9))) + except Exception: brueest = old_meta.get("oeff_brueest", 0.9) + else: + brueest = 0.0 + try: rahmen_b = float(p.get("rahmenB", old_meta.get("oeff_rahmen_b", 0.06))) + except Exception: rahmen_b = old_meta.get("oeff_rahmen_b", 0.06) + try: rahmen_t = float(p.get("rahmenTiefe", old_meta.get("oeff_rahmen_tiefe", 0.08))) + except Exception: rahmen_t = old_meta.get("oeff_rahmen_tiefe", 0.08) + rahmen_p = p.get("rahmenPos", old_meta.get("oeff_rahmen_pos", "mid")) + if rahmen_p not in _OEFF_RAHMEN_POS_OPTIONS: rahmen_p = "mid" + try: fluegel = int(p.get("fluegel", old_meta.get("oeff_fluegel", 1))) + except Exception: fluegel = old_meta.get("oeff_fluegel", 1) + if fluegel < 1: fluegel = 1 + simsa = p.get("simsAus", old_meta.get("oeff_sims_aus", "standard" if otyp == "fenster" else "ohne")) + simsi = p.get("simsIn", old_meta.get("oeff_sims_in", "standard" if otyp == "fenster" else "ohne")) + # Legacy: bool von alter UI - in String konvertieren + if isinstance(simsa, bool): simsa = "standard" if simsa else "ohne" + if isinstance(simsi, bool): simsi = "standard" if simsi else "ohne" + if simsa not in _OEFF_SIMS_STYLES: simsa = "ohne" + if simsi not in _OEFF_SIMS_STYLES: simsi = "ohne" + glas = bool(p.get("glas", old_meta.get("oeff_glas", otyp == "fenster"))) + oref = p.get("oeffReferenz", old_meta.get("oeff_referenz", "mid")) + if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid" + attrs = axis_obj.Attributes + _attach_meta(attrs, wall_id, "oeffnung_point", + old_meta["geschoss"], old_meta["dicke"], + "", "", "mid", + oeff_typ=otyp, + oeff_parent=old_meta.get("oeff_parent", ""), + oeff_breite=breite, oeff_hoehe=hoehe, + oeff_brueest=brueest, + oeff_rahmen_b=rahmen_b, + oeff_rahmen_tiefe=rahmen_t, + oeff_rahmen_pos=rahmen_p, + oeff_fluegel=fluegel, + oeff_sims_aus=simsa, oeff_sims_in=simsi, + oeff_glas=glas, + oeff_referenz=oref) + axis_obj.Attributes = attrs + axis_obj.CommitChanges() + parent_id = old_meta.get("oeff_parent", "") + if parent_id: + _regenerate_element(doc, parent_id) + doc.Views.Redraw() + self._send_state() + return + # Neue Werte mergen + geschoss = p.get("geschoss", old_meta["geschoss"]) + try: dicke = float(p.get("dicke", old_meta["dicke"])) + except Exception: dicke = old_meta["dicke"] + uk_over = p.get("ukOverride", old_meta["uk_override"]) + ok_over = p.get("okOverride", old_meta["ok_override"]) + referenz = p.get("referenz", old_meta.get("referenz", "mid")) + if referenz not in ("mid", "left", "right"): referenz = "mid" + # Dach-spezifische Felder + try: neigung = float(p.get("neigung", old_meta.get("neigung", 30.0))) + except Exception: neigung = old_meta.get("neigung", 30.0) + try: eave_idx = int(p.get("eaveIdx", old_meta.get("eave_idx", 0))) + except Exception: eave_idx = old_meta.get("eave_idx", 0) + dach_typ = p.get("dachTyp", old_meta.get("dach_typ", "pult")) + if dach_typ not in ("pult", "sattel", "walm", "mansarde"): dach_typ = "pult" + try: neigung_unten = float(p.get("neigungUnten", old_meta.get("neigung_unten", 60.0))) + except Exception: neigung_unten = old_meta.get("neigung_unten", 60.0) + try: knick_h = float(p.get("knickH", old_meta.get("knick_h", 2.0))) + except Exception: knick_h = old_meta.get("knick_h", 2.0) + dach_variante = p.get("dachVariante", old_meta.get("dach_variante", "walm")) + if dach_variante not in ("walm", "giebel", "walm_giebel"): dach_variante = "walm" + # Source-Attributes updaten + attrs = axis_obj.Attributes + # Bei Geschoss-Wechsel: Layer wechseln (passend zum Element-Typ) + if geschoss != old_meta["geschoss"]: + g = _geschoss_by_id(doc, geschoss) + geschoss_name = g.get("name", "EG") if g else "EG" + if old_meta["type"] == "wand_axis": + attrs.LayerIndex = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) + elif old_meta["type"] == "decke_outline": + attrs.LayerIndex = _ensure_layer(doc, _layer_path_decke(doc, geschoss_name)) + elif old_meta["type"] == "dach_outline": + attrs.LayerIndex = _ensure_layer(doc, _layer_path_dach(doc, geschoss_name)) + _attach_meta(attrs, wall_id, old_meta["type"], geschoss, dicke, + uk_over, ok_over, referenz, + neigung=neigung, eave_idx=eave_idx, dach_typ=dach_typ, + neigung_unten=neigung_unten, knick_h=knick_h, + dach_variante=dach_variante) + axis_obj.Attributes = attrs + axis_obj.CommitChanges() + # Volumen regenerieren (Layer ggf. anpassen) + _regenerate_volume(doc, wall_id) + doc.Views.Redraw() + self._send_state() + + def _delete_wall(self, wall_id): + """Achse + Volumen + Children loeschen. Bei Oeffnung wird die + Elternwand nach dem Loeschen regeneriert (Loch verschwindet).""" + doc = Rhino.RhinoDoc.ActiveDoc + if not wall_id: return + # Check ob es eine Oeffnung ist + src_obj, src_meta = _find_source(doc, wall_id) + parent_id = None + if src_meta and src_meta["type"] == "oeffnung_point": + parent_id = src_meta.get("oeff_parent") or None + # Wenn eine Wand geloescht wird: zugehoerige Oeffnungen kaskadieren + cascade_ids = [] + if src_meta and src_meta["type"] == "wand_axis": + for op_obj, op_meta in _find_openings_for_wall(doc, wall_id): + cascade_ids.append(op_meta["id"]) + for cid in cascade_ids: + for obj, _m in _find_objects_by_wall_id(doc, cid): + try: doc.Objects.Delete(obj.Id, True) + except Exception: pass + # Haupt-Element loeschen + for obj, meta in _find_objects_by_wall_id(doc, wall_id): + try: doc.Objects.Delete(obj.Id, True) + except Exception as ex: print("[ELEMENTE] delete:", ex) + # Bei Oeffnung-Delete: Elternwand regen + if parent_id: + _regenerate_element(doc, parent_id) + doc.Views.Redraw() + self._send_state() + + def _regenerate_all(self): + """Alle Elemente (Waende + Decken) neu generieren — nuetzlich nach + Geschoss-Aenderung.""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + seen = set() + for obj in list(doc.Objects): + meta = _read_meta(obj) + if meta is None: continue + if meta["type"] not in SOURCE_TYPES: continue + if meta["id"] in seen: continue + seen.add(meta["id"]) + _regenerate_element(doc, meta["id"]) + doc.Views.Redraw() + self._send_state() + + +# --- Event-Listener --------------------------------------------------------- + +# Re-Entry-Guard: wenn _regenerate_volume die Brep ersetzt, feuert das +# Rhino-Event nochmal — wir wollen nicht in eine Schleife geraten. +_REGEN_BUSY = "_elemente_regen_busy" + +# Pending-Regenerate-Queue: alle wall_ids die beim naechsten Idle-Tick +# regeneriert werden sollen. Debounct mehrfache Replace-Events waehrend +# eines Gumball-Drags. +def _pending_set(): + s = sc.sticky.get("elemente_pending_regen") + if s is None: + s = set() + sc.sticky["elemente_pending_regen"] = s + return s + + +def _queue_regen(wall_id): + _pending_set().add(wall_id) + + +def _on_object_replaced(sender, e): + """Wenn eine Wand-Achse verschoben/skaliert/sub-object-editiert wird + → Regeneration queuen (wird beim naechsten Idle-Tick ausgefuehrt).""" + if sc.sticky.get(_REGEN_BUSY): return + try: + # Beide Seiten probieren — manchmal verliert e.NewRhinoObject die + # UserStrings beim Replace-Roundtrip. + meta = None + try: meta = _read_meta(e.NewRhinoObject) + except Exception: pass + if meta is None: + try: meta = _read_meta(e.OldRhinoObject) + except Exception: pass + if meta is None or meta.get("type") not in SOURCE_TYPES: + return + try: + new_obj = e.NewRhinoObject + if new_obj and not _read_meta(new_obj): + attrs = new_obj.Attributes + _attach_meta(attrs, meta["id"], meta["type"], meta["geschoss"], + meta["dicke"], meta["uk_override"], meta["ok_override"], + meta.get("referenz", "mid"), + neigung=meta.get("neigung"), + eave_idx=meta.get("eave_idx"), + dach_typ=meta.get("dach_typ"), + neigung_unten=meta.get("neigung_unten"), + knick_h=meta.get("knick_h"), + dach_variante=meta.get("dach_variante"), + oeff_typ=meta.get("oeff_typ") or None, + oeff_parent=meta.get("oeff_parent") or None, + oeff_breite=meta.get("oeff_breite"), + oeff_hoehe=meta.get("oeff_hoehe"), + oeff_brueest=meta.get("oeff_brueest"), + oeff_rahmen_b=meta.get("oeff_rahmen_b"), + oeff_rahmen_tiefe=meta.get("oeff_rahmen_tiefe"), + oeff_rahmen_pos=meta.get("oeff_rahmen_pos"), + oeff_fluegel=meta.get("oeff_fluegel"), + oeff_sims_aus=meta.get("oeff_sims_aus"), + oeff_sims_in=meta.get("oeff_sims_in"), + oeff_glas=meta.get("oeff_glas"), + oeff_referenz=meta.get("oeff_referenz"), + geschoss_end=meta.get("geschoss_end"), + treppe_breite=meta.get("treppe_breite"), + treppe_n=meta.get("treppe_n"), + treppe_referenz=meta.get("treppe_referenz"), + treppe_modus=meta.get("treppe_modus"), + treppe_lauf_d=meta.get("treppe_lauf_d"), + treppe_art=meta.get("treppe_art"), + treppe_h_over=meta.get("treppe_h_over"), + treppe_soll=meta.get("treppe_soll")) + new_obj.Attributes = attrs + new_obj.CommitChanges() + except Exception: pass + # Wenn eine Wand-Achse veraendert wurde: alle daran haengenden + # Oeffnungs-Points entlang der neuen Achse migrieren (sticky). + if meta.get("type") == "wand_axis": + try: + old_geom = e.OldRhinoObject.Geometry if e.OldRhinoObject else None + new_geom = e.NewRhinoObject.Geometry if e.NewRhinoObject else None + _migrate_openings_to_new_axis(meta["id"], old_geom, new_geom) + except Exception as ex: + print("[ELEMENTE] migrate openings:", ex) + _queue_regen(meta["id"]) + except Exception as ex: + print("[ELEMENTE] on_object_replaced:", ex) + + +def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom): + """Verschiebt alle Oeffnungs-Points einer Wand mit, wenn deren Achse + veraendert wird. Mapping ueber relative Bogenlaenge: ein Oeffnungs- + Punkt bei 30 % der alten Kurve sitzt nachher bei 30 % der neuen. + So bleiben die Oeffnungen 'sticky' an der Wand bei Verschieben, + Drehen, Skalieren oder Reshape der Achse.""" + if not isinstance(old_geom, rg.Curve) or not isinstance(new_geom, rg.Curve): + return + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + try: + old_len = old_geom.GetLength() + new_len = new_geom.GetLength() + except Exception: return + if old_len < 1e-9 or new_len < 1e-9: return + + _was_busy = sc.sticky.get(_REGEN_BUSY, False) + sc.sticky[_REGEN_BUSY] = True + try: + for op_obj, op_meta in _find_openings_for_wall(doc, wall_id): + try: + pt_geom = op_obj.Geometry + if hasattr(pt_geom, 'Location'): + cur_pos = pt_geom.Location + elif isinstance(pt_geom, rg.Point3d): + cur_pos = pt_geom + else: + continue + ok_old, t_old = old_geom.ClosestPoint(cur_pos) + if not ok_old: continue + # Bogenlaenge auf alter Kurve bis t_old → relative Position + sub = rg.Interval(old_geom.Domain.Min, t_old) + try: arc_old = old_geom.GetLength(sub) + except Exception: + # Fallback: lineare Parameter-Interpolation + dom_len = old_geom.Domain.Length + arc_old = ((t_old - old_geom.Domain.Min) / dom_len) * old_len + relative = arc_old / old_len if old_len > 1e-9 else 0.0 + if relative < 0: relative = 0 + if relative > 1: relative = 1 + arc_new = relative * new_len + # Parameter auf neuer Kurve bei dieser Bogenlaenge + lp = new_geom.LengthParameter(arc_new) + # LengthParameter Rueckgabe ist (bool, double) Tuple in IronPython3 + t_new = None + if isinstance(lp, tuple) and len(lp) >= 2 and lp[0]: + t_new = lp[1] + if t_new is None: + # Fallback: lineare Parameter-Interpolation + new_dom = new_geom.Domain + t_new = new_dom.Min + relative * new_dom.Length + new_pos = new_geom.PointAt(t_new) + doc.Objects.Replace(op_obj.Id, rg.Point(new_pos)) + except Exception as ex: + print("[ELEMENTE] migrate one opening:", ex) + finally: + sc.sticky[_REGEN_BUSY] = _was_busy + + +def _count_same_id_type(doc, element_id, type_): + n = 0 + for obj in doc.Objects: + m = _read_meta(obj) + if m and m["id"] == element_id and m["type"] == type_: + n += 1 + if n > 1: return n + return n + + +def _on_object_added(sender, e): + """Faengt Duplikate ab (Copy/Mirror/Rotate-Copy): Rhino kopiert die + UserStrings auf das neue Objekt mit. Source-Duplikate kriegen eine + neue UUID, Volume-Duplikate werden geloescht (Regen baut das neue + Volumen am richtigen Ort).""" + if sc.sticky.get(_REGEN_BUSY): return + try: + new_obj = e.TheObject + meta = _read_meta(new_obj) + if meta is None: return + doc = Rhino.RhinoDoc.ActiveDoc + same_count = _count_same_id_type(doc, meta["id"], meta["type"]) + if same_count <= 1: + return # einziges Objekt mit dieser id, kein Duplikat + + if meta["type"] in SOURCE_TYPES: + # Source-Duplikat: neue ID + Volumen regenerieren + if meta["type"] == "wand_axis": prefix = "wall_" + elif meta["type"] == "decke_outline": prefix = "decke_" + elif meta["type"] == "dach_outline": prefix = "dach_" + elif meta["type"] == "oeffnung_point": + prefix = "fenster_" if meta.get("oeff_typ") == "fenster" else "tuer_" + elif meta["type"] == "treppe_axis": prefix = "treppe_" + else: prefix = "elem_" + new_id = prefix + uuid.uuid4().hex[:10] + attrs = new_obj.Attributes + _attach_meta(attrs, new_id, meta["type"], meta["geschoss"], + meta["dicke"], meta["uk_override"], meta["ok_override"], + meta.get("referenz", "mid"), + neigung=meta.get("neigung"), + eave_idx=meta.get("eave_idx"), + dach_typ=meta.get("dach_typ"), + neigung_unten=meta.get("neigung_unten"), + knick_h=meta.get("knick_h"), + dach_variante=meta.get("dach_variante"), + oeff_typ=meta.get("oeff_typ") or None, + oeff_parent=meta.get("oeff_parent") or None, + oeff_breite=meta.get("oeff_breite"), + oeff_hoehe=meta.get("oeff_hoehe"), + oeff_brueest=meta.get("oeff_brueest")) + new_obj.Attributes = attrs + new_obj.CommitChanges() + print("[ELEMENTE] Source-Duplikat erkannt — neue ID {}".format(new_id)) + _queue_regen(new_id) + elif meta["type"] in VOLUME_TYPES: + # Volume-Duplikat: das mit-kopierte Volumen ist verwaist, + # weil das Source-Duplikat eine neue ID bekommt. Loeschen — + # die Regen-Pipeline erstellt das richtige Volumen am + # korrekten Ort fuer die neue ID. + try: doc.Objects.Delete(new_obj.Id, True) + except Exception as ex: print("[ELEMENTE] dup-volume delete:", ex) + except Exception as ex: + print("[ELEMENTE] on_object_added:", ex) + + +def _on_object_deleted(sender, e): + """Wenn das Source-Objekt (Achse/Outline/Oeffnungs-Point) manuell + geloescht wird → verknuepftes Volumen entfernen. Bei Oeffnung: + Elternwand regenerieren damit das Loch verschwindet.""" + try: + obj = e.TheObject + meta = _read_meta(obj) + if meta and meta.get("type") in SOURCE_TYPES: + doc = Rhino.RhinoDoc.ActiveDoc + vol = _find_target_volume(doc, meta["id"]) + if vol is not None: + doc.Objects.Delete(vol.Id, True) + if meta["type"] == "oeffnung_point": + parent_id = meta.get("oeff_parent") + if parent_id: + _queue_regen(parent_id) + b = sc.sticky.get("elemente_bridge") + if b is not None: b._send_state() + except Exception as ex: + print("[ELEMENTE] on_object_deleted:", ex) + + +_SELECT_BUSY = "_elemente_select_busy" +# Welche Typen werden gekoppelt? source ↔ volume bidirectional. +# Aktuell Wand + Decke. Wenn's gut funktioniert auch fuer Dach, Treppe, Oeffnung. +_PAIRED_VOLUME_TYPES = ("wand_volume", "decke_volume") +_PAIRED_SOURCE_TYPES = ("wand_axis", "decke_outline") + + +def _collect_partners(doc, rhino_objects): + """Sammelt Partner-Objekte fuer Selection-Sync und die Source-Objekte + die Grips brauchen. Liefert (partners_list, sources_with_grips_list).""" + partners = [] + sources = [] + seen_partner_ids = set() + seen_source_ids = set() + for obj in rhino_objects: + meta = _read_meta(obj) + if meta is None: continue + t = meta.get("type", "") + if t in _PAIRED_VOLUME_TYPES: + src, _ = _find_source(doc, meta["id"]) + if src is not None: + if str(src.Id) not in seen_partner_ids: + partners.append(src) + seen_partner_ids.add(str(src.Id)) + if str(src.Id) not in seen_source_ids: + sources.append(src) + seen_source_ids.add(str(src.Id)) + elif t in _PAIRED_SOURCE_TYPES: + vol = _find_target_volume(doc, meta["id"]) + if vol is not None: + if str(vol.Id) not in seen_partner_ids: + partners.append(vol) + seen_partner_ids.add(str(vol.Id)) + if str(obj.Id) not in seen_source_ids: + sources.append(obj) + seen_source_ids.add(str(obj.Id)) + return partners, sources + + +def _on_select_objects(sender, e): + """ArchiCAD-Style bidirektionaler Selection-Sync: + - Klick auf Volumen (Wand/Decke) → Source-Achse mitselektieren + Grips an + - Klick auf Source-Achse → Volumen mitselektieren + Grips an + + So bewegen sich beide synchron bei Move/Gumball, und die Endpunkte + der Lauflinie sind als Grips zum Drag verfuegbar.""" + if sc.sticky.get(_SELECT_BUSY): return + if sc.sticky.get(_REGEN_BUSY): return + try: + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + partners, sources = _collect_partners(doc, e.RhinoObjects) + if not partners and not sources: return + sc.sticky[_SELECT_BUSY] = True + try: + # Partner selektieren — idempotent + for p in partners: + try: + if p.IsSelected(False) == 0: + doc.Objects.Select(p.Id, True) + except Exception as ex: + print("[ELEMENTE] select partner:", ex) + # Grips an Source — idempotent + for s in sources: + try: + if not s.GripsOn: + s.GripsOn = True + s.CommitChanges() + except Exception as ex: + print("[ELEMENTE] grips on:", ex) + finally: + sc.sticky[_SELECT_BUSY] = False + except Exception as ex: + print("[ELEMENTE] on_select:", ex) + + +def _on_deselect_objects(sender, e): + """Bidirektional zu _on_select_objects: + - Volume deselektiert → Source deselektieren + Grips aus + - Source deselektiert → Volume deselektieren + Grips aus""" + if sc.sticky.get(_SELECT_BUSY): return + if sc.sticky.get(_REGEN_BUSY): return + try: + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + partners, sources = _collect_partners(doc, e.RhinoObjects) + if not partners and not sources: return + sc.sticky[_SELECT_BUSY] = True + try: + for p in partners: + try: + if p.IsSelected(False) > 0: + doc.Objects.Select(p.Id, False) + except Exception as ex: + print("[ELEMENTE] deselect partner:", ex) + for s in sources: + try: + if s.GripsOn: + s.GripsOn = False + s.CommitChanges() + except Exception as ex: + print("[ELEMENTE] grips off:", ex) + finally: + sc.sticky[_SELECT_BUSY] = False + except Exception as ex: + print("[ELEMENTE] on_deselect:", ex) + + +def _on_idle_selection(sender, e): + """Pollt periodisch die Selektion + verarbeitet Pending-Regenerate-Queue. + Queue-Verarbeitung debounct mehrfache Replace-Events waehrend eines + Gumball-Drags — pro Idle-Tick wird jede angefragte Wand einmal regeneriert.""" + b = sc.sticky.get("elemente_bridge") + if b is None: return + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + + # 1) Pending Regenerations abarbeiten (sofort, jeden Idle) + pending = _pending_set() + if pending: + ids = list(pending) + pending.clear() + sc.sticky[_REGEN_BUSY] = True + try: + for wid in ids: + try: _regenerate_volume(doc, wid) + except Exception as ex: print("[ELEMENTE] regen", wid, ex) + try: doc.Views.Redraw() + except Exception: pass + finally: + sc.sticky[_REGEN_BUSY] = False + # Bridge updaten — Volumen-Properties (UK/OK) koennten sich + # geaendert haben durch die Edit-Aktion + try: b._send_state() + except Exception: pass + + # 2) Selektions-Poll (langsamer, ~5/s) + try: + b._idle_count = getattr(b, "_idle_count", 0) + 1 + if b._idle_count < 10: return + b._idle_count = 0 + ids = tuple(sorted(str(o.Id) for o in doc.Objects.GetSelectedObjects(False, False))) + if ids != getattr(b, "_last_selection_ids", ()): + b._last_selection_ids = ids + b._send_state() + except Exception: + pass + + +def _install_listeners(bridge): + flag = "elemente_listeners" + sc.sticky["elemente_bridge"] = bridge + if sc.sticky.get(flag): + return + Rhino.RhinoDoc.ReplaceRhinoObject += _on_object_replaced + Rhino.RhinoDoc.AddRhinoObject += _on_object_added + Rhino.RhinoDoc.DeleteRhinoObject += _on_object_deleted + Rhino.RhinoDoc.SelectObjects += _on_select_objects + Rhino.RhinoDoc.DeselectObjects += _on_deselect_objects + Rhino.RhinoApp.Idle += _on_idle_selection + sc.sticky[flag] = True + print("[ELEMENTE] Listener aktiv (Replace + Add + Delete + Select + Idle)") + + +def _bridge_factory(): + b = ElementeBridge() + _install_listeners(b) + return b + + +panel_base.register_and_open("elemente", "ELEMENTE", PANEL_GUID_STR, + _bridge_factory, icon_spec=("E", "#5fa896")) diff --git a/rhino/gestaltung.py b/rhino/gestaltung.py new file mode 100644 index 0000000..341d222 --- /dev/null +++ b/rhino/gestaltung.py @@ -0,0 +1,1635 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +gestaltung.py +GESTALTUNG-Panel: Attribute der Selektion (Farbe, Stiftdicke, Linientyp, +Hatch-Fuellung). +""" +import os +import sys +import math +import json +import time +import Rhino +import Rhino.Geometry as rg +import scriptcontext as sc +import System +import System.Drawing as Drawing + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import panel_base + +PANEL_GUID_STR = "4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829" + +_FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer +_FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject +_LW_FROM_LAYER = Rhino.DocObjects.ObjectPlotWeightSource.PlotWeightFromLayer +_LW_FROM_OBJECT = Rhino.DocObjects.ObjectPlotWeightSource.PlotWeightFromObject +_LT_FROM_LAYER = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromLayer +_LT_FROM_OBJECT = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromObject +# Print-Pendants: ohne die plottet eine Hatch mit eigener Display-Farbe in +# Layerfarbe (= gleiche Farbe wie der Stift). Mit PlotColorFromObject + +# PlotColor folgt der Druck der gewuenschten Hatch-Farbe. +_PLOT_FROM_LAYER = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromLayer +_PLOT_FROM_OBJECT = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject + + +def _sync_plot_color_to_display(attrs): + """Spiegelt ColorSource/ObjectColor in PlotColorSource/PlotColor. + Wird ueberall aufgerufen wo wir eine Hatch-Farbe setzen, damit Print = Display.""" + try: + cs = int(attrs.ColorSource) + if cs == int(_FROM_OBJECT): + attrs.PlotColorSource = _PLOT_FROM_OBJECT + attrs.PlotColor = attrs.ObjectColor + else: + attrs.PlotColorSource = _PLOT_FROM_LAYER + except Exception as ex: + print("[GESTALTUNG] sync plot-color:", ex) + +_FILL_KEY = "ebenen_fill_hatch_id" +_FILL_SOURCE_KEY = "ebenen_fill_source" # "layer" oder "object" +_FILL_OWNER_KEY = "ebenen_fill_owner" # Curve-ID, auf Hatch gesetzt +_NO_FILL_KEY = "ebenen_no_fill" # "1" wenn User Fuellung explizit aus hat + +# Loop-Guard fuer Live-Update +_processing = set() + +# Sticky-Mapping curve_id_str -> hatch_id_str. Wird beim Anlegen jeder Hatch +# gefuellt und beim on_delete als Fallback gelesen, falls Rhino die UserStrings +# der geloeschten Curve schon weggewischt hat. +def _link_curve_hatch(curve_id, hatch_id): + m = sc.sticky.get("gestaltung_curve_hatch") + if not isinstance(m, dict): + m = {} + sc.sticky["gestaltung_curve_hatch"] = m + m[str(curve_id)] = str(hatch_id) + +def _lookup_hatch_for_curve(curve_id): + m = sc.sticky.get("gestaltung_curve_hatch") + if isinstance(m, dict): + return m.get(str(curve_id)) + return None + +def _unlink_curve(curve_id): + m = sc.sticky.get("gestaltung_curve_hatch") + if isinstance(m, dict): + m.pop(str(curve_id), None) + + +# Rhino feuert bei Drag/Move oft on_delete + on_add (statt on_replace). +# Wir merken uns kurz die Hatch-Metadaten bei jedem cascade-delete, damit +# wir die Hatch beim sofortigen Re-Add wiederherstellen koennen. +_PENDING_HATCH_TTL = 3.0 # Sekunden — danach gilt's als echter Delete + +def _save_pending_hatch(curve_id, hatch_obj): + try: + hg = hatch_obj.Geometry + ha = hatch_obj.Attributes + meta = { + "pattern_idx": int(hg.PatternIndex), + "scale": float(hg.PatternScale), + "rotation": float(hg.PatternRotation), + "color_source": int(ha.ColorSource), + "color_argb": int(ha.ObjectColor.ToArgb()), + "fill_source": ha.GetUserString(_FILL_SOURCE_KEY) or "object", + "timestamp": time.time(), + } + except Exception as ex: + print("[GESTALTUNG] save pending-hatch err:", ex) + return + m = sc.sticky.get("gestaltung_pending_hatch") + if not isinstance(m, dict): + m = {} + sc.sticky["gestaltung_pending_hatch"] = m + m[str(curve_id)] = meta + +def _take_pending_hatch(curve_id): + m = sc.sticky.get("gestaltung_pending_hatch") + if not isinstance(m, dict): return None + now = time.time() + expired = [k for k, v in list(m.items()) + if now - v.get("timestamp", 0) > _PENDING_HATCH_TTL] + for k in expired: m.pop(k, None) + return m.pop(str(curve_id), None) + + +def _restore_hatch_from_pending(doc, obj, meta): + """Erzeugt eine Hatch mit den gespeicherten Metadaten (Drag-Recovery).""" + try: + geom = obj.Geometry + except Exception: + return False + if not _is_closed_planar_curve(geom): return False + try: + new_hatches = rg.Hatch.Create(geom, + meta["pattern_idx"], meta["rotation"], meta["scale"], 0.0) + except Exception as ex: + print("[GESTALTUNG] restore Hatch.Create:", ex) + return False + if not new_hatches or len(new_hatches) == 0: return False + new_attrs = Rhino.DocObjects.ObjectAttributes() + new_attrs.LayerIndex = obj.Attributes.LayerIndex + try: + new_attrs.ColorSource = Rhino.DocObjects.ObjectColorSource(meta["color_source"]) + except Exception: + try: new_attrs.ColorSource = _FROM_LAYER + except Exception: pass + try: + new_attrs.ObjectColor = Drawing.Color.FromArgb(meta["color_argb"]) + except Exception: + pass + new_attrs.SetUserString(_FILL_OWNER_KEY, str(obj.Id)) + new_attrs.SetUserString(_FILL_SOURCE_KEY, meta.get("fill_source", "object")) + _sync_plot_color_to_display(new_attrs) + try: + hatch_id = doc.Objects.AddHatch(new_hatches[0], new_attrs) + except Exception as ex: + print("[GESTALTUNG] restore AddHatch:", ex) + return False + if hatch_id == System.Guid.Empty: return False + try: + ca = obj.Attributes.Duplicate() + ca.SetUserString(_FILL_KEY, str(hatch_id)) + _processing.add(obj.Id) + try: doc.Objects.ModifyAttributes(obj, ca, True) + finally: _processing.discard(obj.Id) + except Exception: + pass + _link_curve_hatch(obj.Id, hatch_id) + return True + + +def _color_to_hex(c): + """System.Drawing.Color -> '#rrggbb'. Defensive: IronPython c.R liefert + System.Byte das nicht immer sauber in :02x format einrastet -> int()-Cast.""" + if c is None: + return None + try: + return "#{:02x}{:02x}{:02x}".format(int(c.R), int(c.G), int(c.B)) + except Exception as ex: + print("[GESTALTUNG] color-hex Fehler:", ex) + return None + + +def _hex_to_color(h): + if not isinstance(h, str): h = "888888" + h = h.strip() + if h.startswith("#"): h = h[1:] + if h.startswith(("0x", "0X")): h = h[2:] + if len(h) == 3: # shorthand #rgb -> #rrggbb + h = h[0] * 2 + h[1] * 2 + h[2] * 2 + if len(h) != 6 or any(c not in "0123456789abcdefABCDEF" for c in h): + h = "888888" + return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) + + +def _force_load_linetypes(doc): + """Rhinos Linetype-Tabelle wird lazy initialisiert — wir triggern es.""" + # 1) Eingebaute Methode (falls vorhanden) + for method_name in ("LoadDefaultLinetypes", "LoadDefaults", "LoadStandardLinetypes"): + try: + getattr(doc.Linetypes, method_name)() + return True + except AttributeError: + continue + except Exception: + continue + # 2) Standardnamen suchen triggert internes Laden in einigen Versionen + for name in ("Hidden", "Dashed", "DashDot", "Dots", + "Border", "Center", "Phantom", + "Hidden2", "Dashed2", "DashDot2"): + try: + doc.Linetypes.Find(name, True) + except Exception: + pass + return False + + +def _all_linetypes(doc): + """Liefert alle nicht-geloeschten Linetypes mit Namen. Continuous immer enthalten.""" + _force_load_linetypes(doc) + out = [] + seen = set() + n = 0 + try: + n = doc.Linetypes.Count + except Exception: + pass + for i in range(n): + try: + lt = doc.Linetypes[i] + except Exception: + continue + if lt is None: + continue + try: + if lt.IsDeleted: + continue + except Exception: + pass + try: + name = lt.Name + except Exception: + name = None + if not name or name in seen: + continue + seen.add(name) + out.append(name) + # Continuous immer als erstes — Rhinos Default-Linetype, das oft als + # virtueller Eintrag oder unter anderem Namen verbucht ist. + if "Continuous" not in seen: + out.insert(0, "Continuous") + return out + + +def _all_hatch_patterns(doc): + out = [] + for i in range(doc.HatchPatterns.Count): + hp = doc.HatchPatterns[i] + if hp.IsDeleted: continue + if hp.Name: out.append(hp.Name) + if not out: + out.append("Solid") + return out + + +def _pattern_name(doc, idx): + if idx is None or idx < 0 or idx >= doc.HatchPatterns.Count: + return None + hp = doc.HatchPatterns[idx] + if hp.IsDeleted: return None + return hp.Name + + +def _linetype_name(doc, idx): + if idx is None or idx < 0 or idx >= doc.Linetypes.Count: + return None + lt = doc.Linetypes[idx] + if lt.IsDeleted: + return None + return lt.Name + + +def _is_closed_planar_curve(geom): + return isinstance(geom, rg.Curve) and geom.IsClosed and geom.IsPlanar() + + +def _ebene_fill_for_layer(doc, layer): + """Sucht in dossier_ebenen (doc.Strings) die zur Ebene gehoerige fill-Definition. + Match per dossier_code UserString auf dem Sublayer. + Returns dict {pattern, source, color, scale, rotation} oder None. + """ + if layer is None: return None + try: + code = layer.GetUserString("dossier_code") + except Exception: + code = None + if not code: + print("[GESTALTUNG] _ebene_fill_for_layer: kein dossier_code auf Layer idx={}".format( + getattr(layer, "LayerIndex", "?"))) + return None + raw = doc.Strings.GetValue("dossier_ebenen") + if not raw: + print("[GESTALTUNG] _ebene_fill_for_layer: dossier_ebenen leer in doc.Strings") + return None + try: + ebenen = json.loads(raw) + except Exception as ex: + print("[GESTALTUNG] _ebene_fill_for_layer: json-Fehler:", ex) + return None + if not isinstance(ebenen, list): return None + for e in ebenen: + if not isinstance(e, dict): continue + if e.get("code") != code: continue + f = e.get("fill") + if not isinstance(f, dict): + print("[GESTALTUNG] _ebene_fill_for_layer: Ebene code={} hat KEIN fill-Feld".format(code)) + return None + # lw: Strichstaerke der Hatch-Linien in mm. None = "wie Stift der Ebene" + # (ColorSource/PlotWeightSource bleibt auf FromLayer). + lw_raw = f.get("lw") + lw_val = None + if lw_raw is not None: + try: + v = float(lw_raw) + if v >= 0: lw_val = v + except Exception: + pass + result = { + "pattern": f.get("pattern", "None"), + "source": f.get("source", "layer"), + "color": f.get("color"), + "scale": float(f.get("scale", 1.0)) if f.get("scale") is not None else 1.0, + "rotation": float(f.get("rotation", 0)) if f.get("rotation") is not None else 0.0, + "lw": lw_val, + } + print("[GESTALTUNG] _ebene_fill_for_layer code={} -> {}".format(code, result)) + return result + print("[GESTALTUNG] _ebene_fill_for_layer: code={} nicht in dossier_ebenen gefunden".format(code)) + return None + + +def _apply_ebene_fill(doc, obj): + """Wenn obj geschlossene Kurve auf einer Ebene mit fill-Settings ist, + erzeugt automatisch eine Hatch entsprechend der Ebenen-Definition.""" + if obj is None: return False + try: + attrs = obj.Attributes + except Exception: + return False + # schon gefuellt oder explizit als "keine Fuellung" markiert? + try: + if attrs.GetUserString(_FILL_KEY): return False + if attrs.GetUserString(_NO_FILL_KEY) == "1": return False + except Exception: + pass + try: + geom = obj.Geometry + except Exception: + return False + if not _is_closed_planar_curve(geom): return False + + try: + layer_idx = int(attrs.LayerIndex) + except Exception: + return False + if layer_idx < 0 or layer_idx >= doc.Layers.Count: return False + layer = doc.Layers[layer_idx] + + fill = _ebene_fill_for_layer(doc, layer) + if fill is None: return False + if fill["pattern"] == "None": return False + + pattern_idx = doc.HatchPatterns.Find(fill["pattern"], True) + if pattern_idx < 0: + pattern_idx = doc.HatchPatterns.Find("Solid", True) + if pattern_idx < 0: + pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex + + scale_v = float(fill["scale"]) or 1.0 + rot_rad = math.radians(float(fill["rotation"])) + + # Massstabs-Multiplikator: layer-Skala ist in "Paper-Units" definiert + # (= so wie sie auf dem Druck aussehen soll). Bei eingestelltem 1:N wird + # entsprechend hochskaliert damit die Hatch auf Paper richtig wirkt. + try: + import massstab + m = massstab.get_current_massstab_factor(doc) + if m and m > 0: + scale_v = scale_v * m + except Exception: + pass + + try: + hatches = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0) + except Exception as ex: + print("[GESTALTUNG] Auto-Fill Hatch.Create:", ex) + return False + if not hatches or len(hatches) == 0: return False + + from_layer = (fill["source"] == "layer") + new_attrs = Rhino.DocObjects.ObjectAttributes() + new_attrs.LayerIndex = layer_idx + if from_layer: + new_attrs.ColorSource = _FROM_LAYER + else: + new_attrs.ColorSource = _FROM_OBJECT + new_attrs.ObjectColor = _hex_to_color(fill.get("color") or "#888888") + # Hatch-Strichstaerke: wenn lw definiert -> PlotWeight von Object (Print-aware via massstab) + lw_val = fill.get("lw") + if lw_val is not None: + try: + import massstab as _ms_lw + _ms_lw.write_plotweight(doc, new_attrs, float(lw_val)) + new_attrs.PlotWeightSource = _LW_FROM_OBJECT + except Exception as _ex: + new_attrs.PlotWeightSource = _LW_FROM_OBJECT + new_attrs.PlotWeight = float(lw_val) + new_attrs.SetUserString(_FILL_OWNER_KEY, str(obj.Id)) + new_attrs.SetUserString(_FILL_SOURCE_KEY, "layer") # gekoppelt an Ebene + _sync_plot_color_to_display(new_attrs) + + try: + hatch_id = doc.Objects.AddHatch(hatches[0], new_attrs) + except Exception as ex: + print("[GESTALTUNG] Auto-Fill AddHatch:", ex) + return False + if hatch_id == System.Guid.Empty: return False + + # Wenn Print-Mode aktiv ist, neue Hatch sofort mit Massstab skalieren + try: + import massstab + h_obj = doc.Objects.FindId(hatch_id) + if h_obj is not None: + massstab.post_create_hatch_scale(doc, h_obj, float(fill["scale"]) or 1.0) + except Exception as ex: + print("[GESTALTUNG] post_create_hatch_scale (auto-fill):", ex) + + try: + ca = obj.Attributes.Duplicate() + ca.SetUserString(_FILL_KEY, str(hatch_id)) + _processing.add(obj.Id) + try: doc.Objects.ModifyAttributes(obj, ca, True) + finally: _processing.discard(obj.Id) + except Exception as ex: + print("[GESTALTUNG] Auto-Fill UserString:", ex) + + _link_curve_hatch(obj.Id, hatch_id) + return True + + +def refresh_layer_fills(doc): + """Gleicht Hatches an die aktuellen fill-Settings ihrer zugehoerigen Ebene + an — fuer Hatches die ueber 'Nach Ebene' angelegt wurden (Marker + FILL_SOURCE_KEY=='layer'). Wird beim Apply der Ebenen-Einstellungen + aufgerufen, nicht bei Selection-Events. + + Drei Stufen: + 1) Pattern/Skala/Rotation der bestehenden Hatches anpassen. + 2) Farbe / ColorSource an fill.source + fill.color anpassen — Hatches + mit source=='layer' folgen der Ebenen-Definition. User-Overrides + (source=='object' am Hatch) bleiben unangetastet. + 3) Auto-Fill nachziehen: geschlossene Kurven auf Ebenen mit aktivem + Pattern, die noch keine Hatch UND keinen NO_FILL-Marker haben, + bekommen jetzt eine Hatch (so wirken nachtraeglich definierte + Fuellungen auch auf alte Zeichnungen). + + * Pattern 'None' in der Ebene loescht KEINE Hatches — der User entfernt + Fuellungen explizit ueber die Gestaltung-Panel. + """ + raw = doc.Strings.GetValue("dossier_ebenen") + if not raw: + return 0 + try: + ebenen = json.loads(raw) + except Exception: + return 0 + if not isinstance(ebenen, list): + return 0 + + # Code -> fill-dict fuer schnellen Lookup + fill_by_code = {} + for e in ebenen: + if not isinstance(e, dict): continue + f = e.get("fill") + if isinstance(f, dict) and f.get("pattern") not in (None, "None"): + fill_by_code[e.get("code")] = { + "pattern": f.get("pattern"), + "source": f.get("source", "layer"), + "color": f.get("color"), + "scale": float(f.get("scale", 1.0)) if f.get("scale") is not None else 1.0, + "rotation": float(f.get("rotation", 0.0)) if f.get("rotation") is not None else 0.0, + } + if not fill_by_code: + return 0 + + # --- 1+2) Bestehende Layer-Hatches einsammeln --- + targets = [] + owner_ids = set() + try: + for obj in doc.Objects: + if obj is None: continue + try: + if obj.IsDeleted: continue + except Exception: + continue + try: + attrs = obj.Attributes + if attrs.GetUserString(_FILL_SOURCE_KEY) != "layer": continue + owner_id_str = attrs.GetUserString(_FILL_OWNER_KEY) + except Exception: + continue + if not owner_id_str: continue + try: + owner_id = System.Guid(owner_id_str) + except Exception: + continue + owner = doc.Objects.FindId(owner_id) + if owner is None or owner.IsDeleted: continue + targets.append((obj, owner)) + owner_ids.add(str(owner.Id)) + except Exception as ex: + print("[GESTALTUNG] refresh_layer_fills scan:", ex) + return 0 + + updated = 0 + color_updated = 0 + skipped = 0 + + for hatch_obj, owner in targets: + try: + layer_idx = owner.Attributes.LayerIndex + except Exception: + continue + layer = doc.Layers[layer_idx] if 0 <= layer_idx < doc.Layers.Count else None + try: + code = layer.GetUserString("dossier_code") if layer is not None else None + except Exception: + code = None + fill = fill_by_code.get(code) if code else None + if fill is None: + skipped += 1 + continue + + pattern_idx = doc.HatchPatterns.Find(fill["pattern"], True) + if pattern_idx < 0: + pattern_idx = doc.HatchPatterns.Find("Solid", True) + if pattern_idx < 0: + pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex + scale_v = float(fill["scale"]) or 1.0 + rot_rad = math.radians(float(fill["rotation"])) + # Massstab beachten (siehe _apply_ebene_fill) + try: + import massstab + m = massstab.get_current_massstab_factor(doc) + if m and m > 0: + scale_v = scale_v * m + except Exception: + pass + + # (1) Geometrie-Refresh wenn Pattern/Skala/Drehung sich geaendert haben + try: + hg = hatch_obj.Geometry + cur_p = hg.PatternIndex + cur_s = hg.PatternScale + cur_r = hg.PatternRotation + except Exception: + cur_p, cur_s, cur_r = -1, -1.0, -1.0 + + needs_rebuild = not (cur_p == pattern_idx + and abs(cur_s - scale_v) <= 1e-6 + and abs(cur_r - rot_rad) <= 1e-6) + if needs_rebuild: + try: + geom = owner.Geometry + if _is_closed_planar_curve(geom): + new_h = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0) + if new_h and len(new_h) > 0: + _processing.add(hatch_obj.Id) + try: + doc.Objects.Replace(hatch_obj.Id, new_h[0]) + finally: + _processing.discard(hatch_obj.Id) + updated += 1 + # Print-Mode-aware Skalierung + Original-Update + try: + import massstab as _ms + h_obj = doc.Objects.FindId(hatch_obj.Id) + if h_obj is not None: + _ms.post_create_hatch_scale(doc, h_obj, scale_v) + except Exception as _ex: + print("[GESTALTUNG] post_create_hatch_scale (refresh):", _ex) + except Exception as ex: + print("[GESTALTUNG] refresh rebuild:", ex) + + # (2) Farb-Sync — Hatch mit source=='layer' folgt der Ebenen-Definition + try: + refreshed = doc.Objects.FindId(hatch_obj.Id) or hatch_obj + ha = refreshed.Attributes + want_from_layer = (fill["source"] == "layer") + want_color = _hex_to_color(fill.get("color") or "#888888") + cur_cs = int(ha.ColorSource) + need_change = False + if want_from_layer: + if cur_cs != int(_FROM_LAYER): + need_change = True + else: + if cur_cs != int(_FROM_OBJECT): + need_change = True + else: + try: + if int(ha.ObjectColor.ToArgb()) != int(want_color.ToArgb()): + need_change = True + except Exception: + need_change = True + if need_change: + na = ha.Duplicate() + if want_from_layer: + na.ColorSource = _FROM_LAYER + else: + na.ColorSource = _FROM_OBJECT + na.ObjectColor = want_color + _sync_plot_color_to_display(na) + _processing.add(refreshed.Id) + try: + doc.Objects.ModifyAttributes(refreshed, na, True) + finally: + _processing.discard(refreshed.Id) + color_updated += 1 + except Exception as ex: + print("[GESTALTUNG] refresh color-sync:", ex) + + # (3) Hatch-PlotWeight an fill.lw anpassen (None = wieder ByLayer) + try: + want_lw = fill.get("lw") + refreshed = doc.Objects.FindId(hatch_obj.Id) or hatch_obj + ha = refreshed.Attributes + cur_src = int(ha.PlotWeightSource) + need_lw_change = False + if want_lw is None: + # Auf ByLayer zuruecksetzen + if cur_src != int(_LW_FROM_LAYER): + need_lw_change = True + else: + if cur_src != int(_LW_FROM_OBJECT): + need_lw_change = True + else: + try: + import massstab as _ms_lw_chk + cur_real = _ms_lw_chk.read_plotweight(ha) + if abs(float(cur_real) - float(want_lw)) > 1e-6: + need_lw_change = True + except Exception: + if abs(float(ha.PlotWeight or 0) - float(want_lw)) > 1e-6: + need_lw_change = True + if need_lw_change: + na = ha.Duplicate() + if want_lw is None: + na.PlotWeightSource = _LW_FROM_LAYER + else: + na.PlotWeightSource = _LW_FROM_OBJECT + try: + import massstab as _ms_lw_w + _ms_lw_w.write_plotweight(doc, na, float(want_lw)) + except Exception: + na.PlotWeight = float(want_lw) + _processing.add(refreshed.Id) + try: + doc.Objects.ModifyAttributes(refreshed, na, True) + finally: + _processing.discard(refreshed.Id) + except Exception as ex: + print("[GESTALTUNG] refresh lw-sync:", ex) + + # --- 3) Auto-Fill nachziehen fuer Kurven ohne Hatch --- + added = 0 + # Code -> Sublayer-Indizes (alle Zeichnungsebenen) + try: + layers_by_code = {} + for i in range(doc.Layers.Count): + layer = doc.Layers[i] + if layer is None or layer.IsDeleted: continue + try: + c = layer.GetUserString("dossier_code") + except Exception: + c = None + if c and c in fill_by_code: + layers_by_code.setdefault(c, []).append(i) + + for code, idxs in layers_by_code.items(): + for layer_idx in idxs: + layer = doc.Layers[layer_idx] + try: + curves = list(doc.Objects.FindByLayer(layer)) + except Exception: + continue + for obj in curves: + if obj is None: continue + try: + if obj.IsDeleted: continue + except Exception: + continue + # Hatches selbst ueberspringen (FindByLayer liefert auch sie) + if str(obj.Id) in owner_ids: + continue + try: + ga = obj.Attributes + if ga.GetUserString(_FILL_KEY): continue + if ga.GetUserString(_FILL_OWNER_KEY): continue # ist selbst eine Hatch + if ga.GetUserString(_NO_FILL_KEY) == "1": continue + except Exception: + continue + try: + if not _is_closed_planar_curve(obj.Geometry): continue + except Exception: + continue + try: + if _apply_ebene_fill(doc, obj): + added += 1 + except Exception as ex: + print("[GESTALTUNG] refresh auto-fill:", ex) + except Exception as ex: + print("[GESTALTUNG] refresh auto-fill scan:", ex) + + if updated or color_updated or added: + doc.Views.Redraw() + print("[GESTALTUNG] refresh_layer_fills: pattern={}, farbe={}, neu={}, unveraendert={}".format( + updated, color_updated, added, skipped)) + return updated + color_updated + added + + +def repair_plot_colors(doc): + """Synct PlotColor/PlotColorSource an Color/ColorSource fuer alle Objekte + mit benutzerdefinierter Farbe (ColorSource == FromObject). + + Hintergrund: Rhino fuehrt fuer Anzeige und Druck zwei getrennte Farb- + Quellen — ColorSource (Display) und PlotColorSource (Plot). Default fuer + Plot ist 'PlotColorFromLayer'. Setzt der User die Display-Farbe ueber, + bleibt der Plot trotzdem auf Layerfarbe haengen -> Anzeige und Druck + weichen ab. Diese Funktion gleicht beides ab. + + Scope: nur Objekte wo ColorSource == FromObject (User hat explizit + ueberschrieben). Objekte mit FromLayer werden nicht angefasst — deren + PlotColorFromLayer Default ist bereits konsistent. + + No-op falls schon synchron. Laeuft beim Panel-Start und nach Apply. + """ + fixed = 0 + scanned = 0 + try: + for obj in doc.Objects: + if obj is None: continue + try: + if obj.IsDeleted: continue + attrs = obj.Attributes + cs = int(attrs.ColorSource) + except Exception: + continue + if cs != int(_FROM_OBJECT): + continue # FromLayer -> Default ist bereits ok + scanned += 1 + try: + pcs = int(attrs.PlotColorSource) + need_pcs = (pcs != int(_PLOT_FROM_OBJECT)) + need_pcol = False + try: + need_pcol = (int(attrs.PlotColor.ToArgb()) != int(attrs.ObjectColor.ToArgb())) + except Exception: + need_pcol = True + if not (need_pcs or need_pcol): + continue + ha = attrs.Duplicate() + _sync_plot_color_to_display(ha) + _processing.add(obj.Id) + try: + doc.Objects.ModifyAttributes(obj, ha, True) + finally: + _processing.discard(obj.Id) + fixed += 1 + except Exception as ex: + print("[GESTALTUNG] repair_plot_colors entry:", ex) + except Exception as ex: + print("[GESTALTUNG] repair_plot_colors scan:", ex) + return 0 + if fixed: + doc.Views.Redraw() + print("[GESTALTUNG] repair_plot_colors: {} Objekte repariert (von {} mit Eigenfarbe gescannt)".format(fixed, scanned)) + return fixed + + +def _safe_layer_label(doc, layer, idx): + """Baut ein ASCII-only Layer-Label aus den dossier_id/dossier_code UserStrings, + um layer.FullPath/Name (kann mit Umlauten auf Mac eine UnicodeDecodeError werfen) + zu vermeiden. Fallback: layer.Name in try/except, sonst Index.""" + try: + code = layer.GetUserString("dossier_code") + except Exception: + code = None + if code: + parent_id_str = None + try: + parent_id_str = str(layer.ParentLayerId) + except Exception: + pass + z_id = None + if parent_id_str and parent_id_str != "00000000-0000-0000-0000-000000000000": + try: + for pl in doc.Layers: + try: + if pl.IsDeleted: continue + if str(pl.Id) == parent_id_str: + z_id = pl.GetUserString("dossier_id") or None + break + except Exception: + continue + except Exception: + pass + return "{}/{}".format(z_id or "?", code) + # Kein DOSSIER-Layer — try Name, dann Index + try: + return layer.Name + except Exception: + return "Layer {}".format(idx) + + +def _selection_summary(doc): + objs = list(doc.Objects.GetSelectedObjects(False, False)) + base = {"count": 0, "linetypes": _all_linetypes(doc), "hatchPatterns": _all_hatch_patterns(doc)} + if not objs: + return base + + color_sources, colors = set(), set() + lw_sources, lws = set(), set() + lt_sources, lts = set(), set() + lt_scales = set() + layer_colors, layer_lws, layer_lts, layer_names = set(), set(), set(), set() + + fill_enabled = set() + fill_colors = set() + fill_sources = set() + fill_patterns = set() + fill_scales = set() + fill_rots = set() + has_closed_curves = False + + for obj in objs: + a = obj.Attributes + color_sources.add(int(a.ColorSource)) + oc = _color_to_hex(a.ObjectColor) + if oc: colors.add(oc) + lw_sources.add(int(a.PlotWeightSource)) + # Print-Mode-aware: zeige im Panel den "echten" PlotWeight, nicht den + # mit dem Massstab-Faktor multiplizierten Display-Wert. + try: + import massstab as _ms + lws.add(round(_ms.read_plotweight(a), 4)) + except Exception: + lws.add(round(a.PlotWeight, 4)) + lt_sources.add(int(a.LinetypeSource)) + ltn = _linetype_name(doc, a.LinetypeIndex) + if ltn: lts.add(ltn) + for prop in ("LinetypePatternLengthScale", "LinetypeScale"): + if hasattr(a, prop): + try: + lt_scales.add(round(float(getattr(a, prop)), 4)) + break + except Exception: + pass + if a.LayerIndex >= 0 and a.LayerIndex < doc.Layers.Count: + layer = doc.Layers[a.LayerIndex] + lc = _color_to_hex(layer.Color) + if lc: layer_colors.add(lc) + try: + import massstab as _ms2 + layer_lws.add(round(_ms2.read_plotweight(layer), 4)) + except Exception: + layer_lws.add(round(layer.PlotWeight, 4)) + ll = _linetype_name(doc, layer.LinetypeIndex) + if ll: layer_lts.add(ll) + # WICHTIG: layer.FullPath/Name liefert auf Mac mit Umlauten (Ä in WAENDE etc.) + # eine UnicodeDecodeError ueber die IronPython<->.NET-Bruecke. Wir benutzen + # stattdessen unsere ASCII-only UserStrings (dossier_id + dossier_code) die wir + # beim Layer-Bau gesetzt haben. + nm = _safe_layer_label(doc, layer, a.LayerIndex) + layer_names.add(nm) + + # Fuellung + if _is_closed_planar_curve(obj.Geometry): + has_closed_curves = True + hatch_id_str = a.GetUserString(_FILL_KEY) + hatch_obj = None + if hatch_id_str: + try: + hatch_obj = doc.Objects.FindId(System.Guid(hatch_id_str)) + except Exception: + hatch_obj = None + if hatch_obj is not None and not hatch_obj.IsDeleted: + fill_enabled.add(True) + ha = hatch_obj.Attributes + # Source aus UserString-Marker, faellt auf ColorSource zurueck + src_marker = None + try: + src_marker = ha.GetUserString(_FILL_SOURCE_KEY) + except Exception: + src_marker = None + if src_marker == "layer": + fill_sources.add("layer") + elif src_marker == "object": + fill_sources.add("object") + elif int(ha.ColorSource) == int(_FROM_LAYER): + fill_sources.add("layer") + else: + fill_sources.add("object") + if int(ha.ColorSource) == int(_FROM_LAYER): + if ha.LayerIndex >= 0 and ha.LayerIndex < doc.Layers.Count: + c = _color_to_hex(doc.Layers[ha.LayerIndex].Color) + if c: fill_colors.add(c) + else: + c = _color_to_hex(ha.ObjectColor) + if c: fill_colors.add(c) + try: + hg = hatch_obj.Geometry + pn = _pattern_name(doc, hg.PatternIndex) + if pn: fill_patterns.add(pn) + # Print-Mode-aware: bei aktivem Print zeigen wir die + # "echte" Skala (= das Original vor der Massstab- + # Multiplikation), nicht den display-skalierten Wert. + eff_scale = hg.PatternScale + try: + orig = hatch_obj.Attributes.GetUserString("dossier_hatch_scale_orig") + if orig: eff_scale = float(orig) + except Exception: pass + fill_scales.add(round(eff_scale, 4)) + fill_rots.add(round(math.degrees(hg.PatternRotation), 2)) + except Exception: + pass + else: + fill_enabled.add(False) + # Tri-State auch ohne Hatch melden: + # NO_FILL_KEY=='1' -> "none" (User hat explizit aus) + # Curve auf DOSSIER-Sublayer -> "layer" (folgt Ebene, aktuell leer) + # sonst -> "none" + try: + no_fill = (a.GetUserString(_NO_FILL_KEY) == "1") + except Exception: + no_fill = False + if no_fill: + fill_sources.add("none") + else: + on_dossier_layer = False + if a.LayerIndex >= 0 and a.LayerIndex < doc.Layers.Count: + try: + tc = doc.Layers[a.LayerIndex].GetUserString("dossier_code") + on_dossier_layer = bool(tc) + except Exception: + on_dossier_layer = False + fill_sources.add("layer" if on_dossier_layer else "none") + + def single(s): + return next(iter(s)) if len(s) == 1 else None + + cs = single(color_sources); ls = single(lw_sources); lts_ = single(lt_sources) + + result = dict(base) + result.update({ + "count": len(objs), + "colorSource": "layer" if cs == int(_FROM_LAYER) else ("object" if cs == int(_FROM_OBJECT) else "mixed"), + "color": single(colors), + "lwSource": "layer" if ls == int(_LW_FROM_LAYER) else ("object" if ls == int(_LW_FROM_OBJECT) else "mixed"), + "lw": single(lws), + "linetypeSource": "layer" if lts_ == int(_LT_FROM_LAYER) else ("object" if lts_ == int(_LT_FROM_OBJECT) else "mixed"), + "linetype": single(lts), + "linetypeScale": single(lt_scales), + "layerColor": single(layer_colors), + "layerLw": single(layer_lws), + "layerLinetype": single(layer_lts), + "layerName": single(layer_names), + "canFill": has_closed_curves, + "fillEnabled": single(fill_enabled), + "fillColor": single(fill_colors), + "fillSource": single(fill_sources), + "fillPattern": single(fill_patterns), + "fillScale": single(fill_scales), + "fillRotation": single(fill_rots), + "hatchPatterns": _all_hatch_patterns(doc), + }) + print("[GESTALTUNG] sel: n={} colorSrc={} color={} layerColor={}".format( + result.get("count"), result.get("colorSource"), + result.get("color"), result.get("layerColor"))) + return result + + +class GestaltungBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "gestaltung") + + def _on_ready(self): + doc = Rhino.RhinoDoc.ActiveDoc + try: + before = doc.Linetypes.Count + ok = _force_load_linetypes(doc) + after = doc.Linetypes.Count + print("[GESTALTUNG] Linetypes vor: {}, nach LoadDefaults({}): {}".format(before, ok, after)) + entries = [] + for i in range(after): + lt = doc.Linetypes[i] + if lt is None: continue + try: flags = "del" if lt.IsDeleted else ("ref" if lt.IsReference else "ok") + except Exception: flags = "?" + try: nm = lt.Name + except Exception: nm = "?" + entries.append("[{}] {} ({})".format(i, nm, flags)) + print("[GESTALTUNG] {}".format(" | ".join(entries))) + except Exception as ex: + print("[GESTALTUNG] Linetype-Diagnose:", ex) + # One-Shot Repair: aeltere Hatches (vor dem PlotColor-Fix angelegt) + # bekommen ihre Print-Attribute mit Display synchronisiert. + try: + repair_plot_colors(doc) + except Exception as ex: + print("[GESTALTUNG] repair on ready:", ex) + self._send_selection() + + def handle(self, data): + if not isinstance(data, dict): + return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): + p = {} + + if t == "READY": + self._on_ready() + elif t == "GET_SELECTION": + self._send_selection() + elif t == "SET_COLOR_SOURCE": + self._set_color_source(p.get("source", "layer"), p.get("color")) + elif t == "SET_LW_SOURCE": + self._set_lw_source(p.get("source", "layer"), p.get("lw")) + elif t == "SET_LINETYPE_SOURCE": + self._set_linetype_source(p.get("source", "layer"), p.get("name")) + elif t == "SET_LINETYPE_SCALE": + self._set_linetype_scale(p.get("scale")) + elif t == "SET_FILL": + self._set_fill( + bool(p.get("enabled")), + p.get("source", "object"), + p.get("color"), + p.get("pattern"), + p.get("scale"), + p.get("rotation"), + ) + + def _send_selection(self): + doc = Rhino.RhinoDoc.ActiveDoc + try: + self.send("SELECTION", _selection_summary(doc)) + except Exception as ex: + print("[GESTALTUNG] Selection:", ex) + + # ---- Attribute-Setter ------------------------------------------------ + + def _modify_each(self, mutator): + """mutator(attrs) muss die Attrs in-place anpassen.""" + doc = Rhino.RhinoDoc.ActiveDoc + objs = list(doc.Objects.GetSelectedObjects(False, False)) + for obj in objs: + a = obj.Attributes.Duplicate() + mutator(a, obj) + doc.Objects.ModifyAttributes(obj, a, True) + doc.Views.Redraw() + self._send_selection() + + def _set_color_source(self, source, color_hex): + col = _hex_to_color(color_hex) if (source == "object" and color_hex) else None + def m(a, _obj): + if source == "layer": + a.ColorSource = _FROM_LAYER + else: + a.ColorSource = _FROM_OBJECT + if col is not None: a.ObjectColor = col + # Plot-Pendant mitspiegeln — sonst druckt eine Curve mit eigener + # Display-Farbe trotzdem in Layerfarbe (PlotColorSource bleibt + # auf Default 'PlotColorFromLayer'). + _sync_plot_color_to_display(a) + self._modify_each(m) + + def _set_lw_source(self, source, lw): + # Print-Mode-aware: bei aktivem Print-View werden PlotWeights skaliert. + # write_plotweight() kuemmert sich um beides (Original-Speicherung + + # Skalierungs-Multiplier). + try: + import massstab + except Exception: + massstab = None + doc = Rhino.RhinoDoc.ActiveDoc + def m(a, _obj): + if source == "layer": + a.PlotWeightSource = _LW_FROM_LAYER + else: + a.PlotWeightSource = _LW_FROM_OBJECT + if lw is not None: + if massstab is not None: + massstab.write_plotweight(doc, a, float(lw)) + else: + a.PlotWeight = float(lw) + self._modify_each(m) + + def _set_linetype_scale(self, scale): + if scale is None: return + try: + s = float(scale) + except Exception: + return + if s <= 0: return + doc = Rhino.RhinoDoc.ActiveDoc + objs = list(doc.Objects.GetSelectedObjects(False, False)) + ok = 0 + for obj in objs: + a = obj.Attributes.Duplicate() + applied = False + # Versuch 1: Attribut-Property (Rhino 8) + for prop in ("LinetypePatternLengthScale", "LinetypeScale"): + if hasattr(a, prop): + try: + setattr(a, prop, s) + doc.Objects.ModifyAttributes(obj, a, True) + applied = True + break + except Exception as ex: + print("[GESTALTUNG] attr {} fehler: {}".format(prop, ex)) + # Versuch 2: direkt auf RhinoObject + if not applied: + for prop in ("LinetypePatternLengthScale", "LinetypeScale"): + if hasattr(obj, prop): + try: + setattr(obj, prop, s) + applied = True + break + except Exception as ex: + print("[GESTALTUNG] obj {} fehler: {}".format(prop, ex)) + if applied: + ok += 1 + doc.Views.Redraw() + if ok == 0: + print("[GESTALTUNG] Linetype-Scale nicht unterstuetzt (Rhino-Version?)") + else: + print("[GESTALTUNG] Linetype-Scale auf {} Objekt(e) angewendet".format(ok)) + self._send_selection() + + def _set_linetype_source(self, source, name): + doc = Rhino.RhinoDoc.ActiveDoc + idx = -1 + if source == "object" and name: + try: + idx = doc.Linetypes.Find(name, True) + except Exception: + idx = -1 + def m(a, _obj): + if source == "layer": + a.LinetypeSource = _LT_FROM_LAYER + else: + a.LinetypeSource = _LT_FROM_OBJECT + if idx >= 0: a.LinetypeIndex = idx + self._modify_each(m) + + # ---- Fuellung (Hatch) ----------------------------------------------- + + def _set_fill(self, enabled, source, color_hex, pattern_name=None, scale=None, rotation_deg=None): + doc = Rhino.RhinoDoc.ActiveDoc + objs = list(doc.Objects.GetSelectedObjects(False, False)) + is_layer_source = (source == "layer") + + # Werte aus React (nur fuer Object-Source relevant) + passed_pattern_idx = -1 + if pattern_name: + passed_pattern_idx = doc.HatchPatterns.Find(pattern_name, True) + if passed_pattern_idx < 0: + passed_pattern_idx = doc.HatchPatterns.Find("Solid", True) + if passed_pattern_idx < 0: + passed_pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex + passed_color = _hex_to_color(color_hex) if color_hex else _hex_to_color("#cccccc") + passed_scale = float(scale) if scale is not None else 1.0 + passed_rot_rad = math.radians(float(rotation_deg)) if rotation_deg is not None else 0.0 + + for obj in objs: + geom = obj.Geometry + if not _is_closed_planar_curve(geom): + continue + a = obj.Attributes + existing_id_str = a.GetUserString(_FILL_KEY) + existing_hatch = None + if existing_id_str: + try: + existing_hatch = doc.Objects.FindId(System.Guid(existing_id_str)) + except Exception: + existing_hatch = None + + # Effektive Werte je nach Source bestimmen + # "Nach Ebene" = die fill-Settings der zugehoerigen DOSSIER-Ebene + # (Pattern/Scale/Rotation/Source/Color aus dem Ebenen-Einstellungen-Dialog). + if is_layer_source: + layer_idx = a.LayerIndex + layer = doc.Layers[layer_idx] if 0 <= layer_idx < doc.Layers.Count else None + fill = _ebene_fill_for_layer(doc, layer) if layer is not None else None + if fill is None or fill["pattern"] == "None": + # "Nach Ebene" aber die Ebene hat KEINE Fuellung definiert: + # nichts erzeugen — Curve in "folgt Ebene, aktuell leer"-Zustand + # setzen, damit sie spaeter Auto-Fill bekommt, sobald die Ebene + # ein Pattern bekommt. KEIN Solid-Fallback (gab eine Solid in + # Stiftfarbe, was nicht gewollt ist). + if existing_hatch is not None and not existing_hatch.IsDeleted: + _processing.add(existing_hatch.Id) + try: doc.Objects.Delete(existing_hatch.Id, True) + finally: _processing.discard(existing_hatch.Id) + try: + ca = obj.Attributes.Duplicate() + ca.SetUserString(_FILL_KEY, "") + ca.SetUserString(_NO_FILL_KEY, "") + _processing.add(obj.Id) + try: doc.Objects.ModifyAttributes(obj, ca, True) + finally: _processing.discard(obj.Id) + except Exception as ex: + print("[GESTALTUNG] _set_fill follow-layer empty:", ex) + continue + else: + pattern_idx = doc.HatchPatterns.Find(fill["pattern"], True) + if pattern_idx < 0: + pattern_idx = doc.HatchPatterns.Find("Solid", True) + if pattern_idx < 0: + pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex + scale_v = float(fill["scale"]) or 1.0 + rot_rad = math.radians(float(fill["rotation"])) + eff_from_layer = (fill["source"] == "layer") + eff_color = _hex_to_color(fill.get("color") or "#888888") if not eff_from_layer else passed_color + else: + pattern_idx = passed_pattern_idx + scale_v = passed_scale + rot_rad = passed_rot_rad + eff_from_layer = False # Eigene Quelle -> Farbe vom Objekt + eff_color = passed_color + + # Massstab-Multiplikator anwenden (Paper-Skala * 1:N). + try: + import massstab + _m = massstab.get_current_massstab_factor(doc) + if _m and _m > 0: + scale_v = scale_v * _m + except Exception: + pass + + if enabled: + # Marker "keine Fuellung" aufheben — User will explizit fuellen + try: + if a.GetUserString(_NO_FILL_KEY): + ca = obj.Attributes.Duplicate() + ca.SetUserString(_NO_FILL_KEY, "") + _processing.add(obj.Id) + try: doc.Objects.ModifyAttributes(obj, ca, True) + finally: _processing.discard(obj.Id) + except Exception: + pass + if existing_hatch is not None and not existing_hatch.IsDeleted: + # Pattern / Scale / Rotation: nur Geometrie ersetzen wenn anders + try: + hg = existing_hatch.Geometry + cur_pattern_idx = hg.PatternIndex + cur_scale = hg.PatternScale + cur_rot = hg.PatternRotation + except Exception: + cur_pattern_idx = pattern_idx + cur_scale = scale_v + cur_rot = rot_rad + needs_rebuild = ( + cur_pattern_idx != pattern_idx + or abs(cur_scale - scale_v) > 1e-6 + or abs(cur_rot - rot_rad) > 1e-6 + ) + if needs_rebuild: + try: + new_hatches = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0) + except Exception: + new_hatches = None + if new_hatches and len(new_hatches) > 0: + _processing.add(existing_hatch.Id) + try: + doc.Objects.Replace(existing_hatch.Id, new_hatches[0]) + finally: + _processing.discard(existing_hatch.Id) + # Replace: Original-Wert + ggf. Print-Skalierung aktualisieren + try: + import massstab as _ms2 + h_obj = doc.Objects.FindId(existing_hatch.Id) + if h_obj is not None: + _ms2.post_create_hatch_scale(doc, h_obj, scale_v) + except Exception as _ex: + print("[GESTALTUNG] post_create_hatch_scale (replace):", _ex) + # Farbe / Source / FILL_SOURCE-Marker aktualisieren + refreshed = doc.Objects.FindId(existing_hatch.Id) or existing_hatch + ha = refreshed.Attributes.Duplicate() + if eff_from_layer: + ha.ColorSource = _FROM_LAYER + else: + ha.ColorSource = _FROM_OBJECT + ha.ObjectColor = eff_color + ha.SetUserString(_FILL_SOURCE_KEY, "layer" if is_layer_source else "object") + _sync_plot_color_to_display(ha) + _processing.add(refreshed.Id) + try: + doc.Objects.ModifyAttributes(refreshed, ha, True) + finally: + _processing.discard(refreshed.Id) + else: + try: + hatches = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0) + except Exception: + hatches = None + if hatches and len(hatches) > 0: + new_attrs = Rhino.DocObjects.ObjectAttributes() + if eff_from_layer: + new_attrs.ColorSource = _FROM_LAYER + else: + new_attrs.ColorSource = _FROM_OBJECT + new_attrs.ObjectColor = eff_color + new_attrs.LayerIndex = a.LayerIndex + new_attrs.SetUserString(_FILL_OWNER_KEY, str(obj.Id)) + new_attrs.SetUserString(_FILL_SOURCE_KEY, + "layer" if is_layer_source else "object") + _sync_plot_color_to_display(new_attrs) + hatch_id = doc.Objects.AddHatch(hatches[0], new_attrs) + if hatch_id != System.Guid.Empty: + ca = obj.Attributes.Duplicate() + ca.SetUserString(_FILL_KEY, str(hatch_id)) + _processing.add(obj.Id) + try: + doc.Objects.ModifyAttributes(obj, ca, True) + finally: + _processing.discard(obj.Id) + _link_curve_hatch(obj.Id, hatch_id) + # Neue Hatch: Print-Mode-aware skalieren + try: + import massstab as _ms + h_obj = doc.Objects.FindId(hatch_id) + if h_obj is not None: + _ms.post_create_hatch_scale(doc, h_obj, scale_v) + except Exception as _ex: + print("[GESTALTUNG] post_create_hatch_scale (set_fill):", _ex) + else: + if existing_hatch is not None and not existing_hatch.IsDeleted: + _processing.add(existing_hatch.Id) + try: + doc.Objects.Delete(existing_hatch.Id, True) + finally: + _processing.discard(existing_hatch.Id) + ca = obj.Attributes.Duplicate() + ca.SetUserString(_FILL_KEY, "") + # Marker setzen: Auto-Fill ueberspringt diese Curve in Zukunft + ca.SetUserString(_NO_FILL_KEY, "1") + _processing.add(obj.Id) + try: + doc.Objects.ModifyAttributes(obj, ca, True) + finally: + _processing.discard(obj.Id) + + doc.Views.Redraw() + self._send_selection() + + +# --- Selection-Events ---------------------------------------------------- + +def _install_selection_listener(bridge): + flag = "gestaltung_selection_listener" + sc.sticky["gestaltung_bridge"] = bridge + if sc.sticky.get(flag): + return + + def refresh(*args): + b = sc.sticky.get("gestaltung_bridge") + if b is not None: + try: b._send_selection() + except Exception: pass + + def on_replace(sender, args): + """Hatch der zugehoerigen Curve mitziehen wenn Curve veraendert wird.""" + new_obj = args.NewRhinoObject + if new_obj is None or new_obj.Id in _processing: + return + a = new_obj.Attributes + hatch_id_str = a.GetUserString(_FILL_KEY) + if not hatch_id_str: + return + print("[GESTALTUNG] on_replace fuer Curve mit Fill") + try: + hatch_id = System.Guid(hatch_id_str) + except Exception: + return + doc = Rhino.RhinoDoc.ActiveDoc + hatch_obj = doc.Objects.FindId(hatch_id) + if hatch_obj is None or hatch_obj.IsDeleted: + return + geom = new_obj.Geometry + if not _is_closed_planar_curve(geom): + return + try: + hg = hatch_obj.Geometry + pattern_idx = hg.PatternIndex + cur_scale = hg.PatternScale + cur_rot = hg.PatternRotation + except Exception: + pattern_idx = doc.HatchPatterns.CurrentHatchPatternIndex + cur_scale = 1.0 + cur_rot = 0.0 + try: + new_hatches = rg.Hatch.Create(geom, pattern_idx, cur_rot, cur_scale, 0.0) + except Exception: + return + if not new_hatches or len(new_hatches) == 0: + return + _processing.add(hatch_id) + try: + doc.Objects.Replace(hatch_id, new_hatches[0]) + except Exception as ex: + print("[GESTALTUNG] Hatch-Update:", ex) + finally: + _processing.discard(hatch_id) + + def on_delete(sender, args): + """Wenn eine Curve geloescht wird, ihre gekoppelte Hatch mitloeschen. + Wenn umgekehrt eine Hatch direkt geloescht wird, den Verweis auf der + Curve aufraeumen damit beim naechsten Toggle keine Geister-Referenz steht.""" + obj = args.TheObject + try: + print("[GESTALTUNG] on_delete fired id={}".format(obj.Id if obj else None)) + except Exception: + pass + if obj is None or obj.Id in _processing: + return + doc = Rhino.RhinoDoc.ActiveDoc + try: + attrs = obj.Attributes + except Exception: + return + + # Pfad A: geloeschte Curve hatte eine Hatch -> Hatch mitloeschen + try: + hatch_id_str = attrs.GetUserString(_FILL_KEY) + except Exception: + hatch_id_str = None + # Fallback: Mapping in sc.sticky (UserStrings koennen nach Delete leer sein) + if not hatch_id_str: + hatch_id_str = _lookup_hatch_for_curve(obj.Id) + if hatch_id_str: + print("[GESTALTUNG] on_delete: hatch via sticky map gefunden") + if hatch_id_str: + try: + hatch_id = System.Guid(hatch_id_str) + except Exception: + hatch_id = None + if hatch_id is not None: + hatch_obj = doc.Objects.FindId(hatch_id) + if hatch_obj is not None and not hatch_obj.IsDeleted: + # Metadaten merken fuer eventuelles Drag-Recovery (Rhino feuert + # bei Drag/Move oft on_delete+on_add statt on_replace) + _save_pending_hatch(obj.Id, hatch_obj) + _processing.add(hatch_id) + try: + ok = doc.Objects.Delete(hatch_id, True) + print("[GESTALTUNG] Curve geloescht -> Hatch {} ({})".format( + "weg" if ok else "konnte nicht geloescht werden", hatch_id)) + except Exception as ex: + print("[GESTALTUNG] Hatch-Loeschen:", ex) + finally: + _processing.discard(hatch_id) + _unlink_curve(obj.Id) + return # Curve-Fall fertig + + # Pfad B: geloeschte Hatch hatte einen Owner-Verweis -> Curve aufraeumen + try: + owner_id_str = attrs.GetUserString(_FILL_OWNER_KEY) + except Exception: + owner_id_str = None + if owner_id_str: + try: + owner_id = System.Guid(owner_id_str) + except Exception: + owner_id = None + if owner_id is not None: + owner_obj = doc.Objects.FindId(owner_id) + if owner_obj is not None and not owner_obj.IsDeleted: + try: + ca = owner_obj.Attributes.Duplicate() + ca.SetUserString(_FILL_KEY, "") + _processing.add(owner_id) + try: + doc.Objects.ModifyAttributes(owner_obj, ca, True) + finally: + _processing.discard(owner_id) + except Exception as ex: + print("[GESTALTUNG] Curve-Verweis aufraeumen:", ex) + + def on_add(sender, args): + """Auto-Fill bzw. Drag-Recovery: neues Objekt -> ggf. Hatch erzeugen. + - Wenn das Objekt eben gerade als Teil eines Drag/Move geloescht wurde, + stellen wir die Hatch mit den gemerkten Metadaten wieder her. + - Sonst pruefen wir ob die Ebene ein Auto-Fill konfiguriert hat.""" + obj = args.TheObject + if obj is None: + return + try: + geom_kind = type(obj.Geometry).__name__ + except Exception: + geom_kind = "?" + if obj.Id in _processing: + return + print("[GESTALTUNG] on_add: id={} type={}".format(obj.Id, geom_kind)) + doc = Rhino.RhinoDoc.ActiveDoc + + # 1) Drag-Recovery: Hatch-Metadaten wurden gerade in on_delete gespeichert? + pending = _take_pending_hatch(obj.Id) + if pending is not None: + try: + ok = _restore_hatch_from_pending(doc, obj, pending) + except Exception as ex: + print("[GESTALTUNG] on_add restore Exception:", ex) + ok = False + if ok: + print("[GESTALTUNG] Drag-Recovery: Hatch wiederhergestellt fuer {}".format(obj.Id)) + b = sc.sticky.get("gestaltung_bridge") + if b is not None: + try: b._send_selection() + except Exception: pass + return + + # 2) Auto-Fill aus Ebenen-Definition + try: + ok = _apply_ebene_fill(doc, obj) + except Exception as ex: + print("[GESTALTUNG] on_add Exception:", ex) + return + print("[GESTALTUNG] on_add ok={}".format(ok)) + if ok: + b = sc.sticky.get("gestaltung_bridge") + if b is not None: + try: b._send_selection() + except Exception: pass + + def on_modify_attrs(sender, args): + """Reagiert auf Attribut-Aenderungen an Objekten: + 1) Curve auf neue Ebene -> gekoppelte Hatch zieht mit + 2) ColorSource -> FromObject -> PlotColorSource/PlotColor mitsynchen + (sonst druckt das Objekt trotz eigener Display-Farbe in Layerfarbe).""" + try: + obj = args.RhinoObject + old_attr = args.OldAttributes + new_attr = args.NewAttributes + old_lyr = old_attr.LayerIndex + new_lyr = new_attr.LayerIndex + except Exception: + return + if obj is None or obj.Id in _processing: + return + + # --- (2) Plot-Color Auto-Sync --- + try: + new_cs = int(new_attr.ColorSource) + if new_cs == int(_FROM_OBJECT): + new_pcs = int(new_attr.PlotColorSource) + need_pcs = (new_pcs != int(_PLOT_FROM_OBJECT)) + need_pcol = False + try: + need_pcol = (int(new_attr.PlotColor.ToArgb()) != int(new_attr.ObjectColor.ToArgb())) + except Exception: + need_pcol = True + if need_pcs or need_pcol: + doc = Rhino.RhinoDoc.ActiveDoc + ha = new_attr.Duplicate() + _sync_plot_color_to_display(ha) + _processing.add(obj.Id) + try: + doc.Objects.ModifyAttributes(obj, ha, True) + finally: + _processing.discard(obj.Id) + except Exception as ex: + print("[GESTALTUNG] on_modify_attrs plot-sync:", ex) + + # --- (1) Layer-Wechsel -> Hatch mitziehen --- + if old_lyr == new_lyr: + return + try: + hatch_id_str = new_attr.GetUserString(_FILL_KEY) + except Exception: + hatch_id_str = None + if not hatch_id_str: + return # nur Curves mit gekoppelter Hatch interessieren uns + try: + hatch_id = System.Guid(hatch_id_str) + except Exception: + return + doc = Rhino.RhinoDoc.ActiveDoc + hatch_obj = doc.Objects.FindId(hatch_id) + if hatch_obj is None or hatch_obj.IsDeleted: + return + try: + ha = hatch_obj.Attributes.Duplicate() + if ha.LayerIndex == new_lyr: + return + ha.LayerIndex = new_lyr + _processing.add(hatch_id) + try: + doc.Objects.ModifyAttributes(hatch_obj, ha, True) + finally: + _processing.discard(hatch_id) + print("[GESTALTUNG] Curve {} Layer geaendert -> Hatch mitgezogen".format(obj.Id)) + except Exception as ex: + print("[GESTALTUNG] on_modify_attrs:", ex) + return + # Falls die neue Ebene andere Fill-Settings hat (Pattern/Skala/Drehung), + # die Hatch entsprechend an die neue Layer-Definition angleichen. + try: + refresh_layer_fills(doc) + except Exception as ex: + print("[GESTALTUNG] on_modify_attrs refresh:", ex) + + Rhino.RhinoDoc.SelectObjects += refresh + Rhino.RhinoDoc.DeselectObjects += refresh + Rhino.RhinoDoc.DeselectAllObjects += refresh + Rhino.RhinoDoc.ReplaceRhinoObject += on_replace + Rhino.RhinoDoc.DeleteRhinoObject += on_delete + Rhino.RhinoDoc.AddRhinoObject += on_add + Rhino.RhinoDoc.ModifyObjectAttributes += on_modify_attrs + sc.sticky[flag] = True + print("[GESTALTUNG] Listener aktiv (Selection + Hatch-Live-Update + Ebene-Auto-Fill + Layer-Sync)") + + +def _bridge_factory(): + b = GestaltungBridge() + _install_selection_listener(b) + return b + + +panel_base.register_and_open("gestaltung", "GESTALTUNG", PANEL_GUID_STR, _bridge_factory, + icon_spec=("G", "#5fa896")) diff --git a/rhino/inspect_section.py b/rhino/inspect_section.py new file mode 100644 index 0000000..373693b --- /dev/null +++ b/rhino/inspect_section.py @@ -0,0 +1,163 @@ +# ! python3 +""" +inspect_section.py +Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log, +ohne dass irgendein Panel-Setup gebraucht wird. + +Aufruf: + _-RunPythonScript "/Users/karim/STUDIO/rhino-panel/rhino/inspect_section.py" +""" +import Rhino + + +def dump(label, obj): + print("--- {} ({})".format(label, type(obj).__name__ if obj is not None else "None")) + if obj is None: + return + for name in dir(obj): + if name.startswith("_"): + continue + try: + v = getattr(obj, name) + except Exception as ex: + print(" {} -> err: {}".format(name, ex)) + continue + if callable(v): + continue + try: + print(" {} = {!r}".format(name, v)) + except Exception: + try: + print(" {} = (unprintable)".format(name)) + except Exception: + pass + + +doc = Rhino.RhinoDoc.ActiveDoc +layer = doc.Layers.CurrentLayer +print("============================================") +print("Aktive Ebene:") +print(" Name =", layer.Name) +try: print(" Id =", layer.Id) +except Exception: pass + +print("\n--- Layer-Properties die mit 'Section' anfangen ---") +for n in dir(layer): + if n.startswith("_"): + continue + if "section" in n.lower() or "hatch" in n.lower() or "fill" in n.lower(): + try: + v = getattr(layer, n) + if callable(v): + continue + print(" layer.{} = {!r}".format(n, v)) + except Exception as ex: + print(" layer.{} -> err: {}".format(n, ex)) + +# layer.SectionStyle dumpen wenn vorhanden +try: + if hasattr(layer, "SectionStyle"): + dump("layer.SectionStyle", layer.SectionStyle) +except Exception as ex: + print(" layer.SectionStyle err:", ex) + +# Via doc.SectionStyles + layer.SectionStyleId +try: + if hasattr(layer, "SectionStyleId"): + sid = layer.SectionStyleId + print("\n layer.SectionStyleId =", sid) + for tname in ("SectionStyles", "SectionAttributes"): + if hasattr(doc, tname): + tbl = getattr(doc, tname) + print(" doc.{} =".format(tname), tbl, "Count:", + getattr(tbl, "Count", "?")) + try: + ss = tbl.FindId(sid) + dump("doc.{}.FindId({})".format(tname, sid), ss) + except Exception as ex: + print(" FindId err:", ex) +except Exception as ex: + print("SectionStyleId-Zweig err:", ex) + +print("\n--- Doc-Tabellen (alles was 'Section' enthaelt) ---") +for n in dir(doc): + if n.startswith("_"): continue + if "section" in n.lower(): + try: + v = getattr(doc, n) + if callable(v): continue + print(" doc.{} = {!r} (count={})".format( + n, v, getattr(v, "Count", "?"))) + except Exception as ex: + print(" doc.{} -> err: {}".format(n, ex)) + +print("\n--- Layer UserDictionary ---") +try: + ud = layer.UserDictionary + cnt = ud.Count + print(" Count:", cnt) + for key in ud.Keys: + try: + v = ud[key] + print(" [{}] = {!r} (type={})".format(key, v, type(v).__name__)) + except Exception as ex: + print(" [{}] err: {}".format(key, ex)) +except Exception as ex: + print(" UserDictionary err:", ex) + +print("\n--- Layer UserStrings (NameValueCollection) ---") +try: + nvc = layer.GetUserStrings() + print(" Count:", nvc.Count) + for k in nvc.AllKeys: + print(" [{}] = {!r}".format(k, nvc[k])) +except Exception as ex: + print(" GetUserStrings err:", ex) +# Fallback: ueber Count + GetUserString +try: + # Manche Rhino-Versionen haben GetUserStringKeys() oder iterieren anders + if hasattr(layer, "GetUserStringKeys"): + keys = layer.GetUserStringKeys() + print(" GetUserStringKeys ->", list(keys)) +except Exception as ex: + print(" GetUserStringKeys err:", ex) + +print("\n--- Layer UserData (Custom .NET Blobs) ---") +try: + udl = layer.UserData + print(" Count:", udl.Count if udl else "None") + if udl: + for i in range(udl.Count): + ud_item = udl[i] + print(" UserData[{}]:".format(i)) + for prop in ("Description", "Name", "Id", "DataCRC", "InstanceId"): + try: + if hasattr(ud_item, prop): + print(" {} = {!r}".format(prop, getattr(ud_item, prop))) + except Exception as ex: + print(" {} err: {}".format(prop, ex)) + print(" type:", type(ud_item).__name__) + print(" full type:", type(ud_item).__module__ + "." + type(ud_item).__name__) + # Dump alle public Attrs + for a in sorted(dir(ud_item)): + if a.startswith("_"): continue + try: + v = getattr(ud_item, a) + if callable(v): continue + print(" .{} = {!r}".format(a, v)) + except Exception: + pass +except Exception as ex: + print(" UserData err:", ex) + +print("\n--- Layer-Properties vollstaendig (alphabetisch) ---") +for n in sorted(dir(layer)): + if n.startswith("_"): continue + try: + v = getattr(layer, n) + if callable(v): continue + print(" layer.{} = {!r}".format(n, v)) + except Exception as ex: + print(" layer.{} -> err: {}".format(n, ex)) + +print("============================================") diff --git a/rhino/layer_builder.py b/rhino/layer_builder.py new file mode 100644 index 0000000..094bd9f --- /dev/null +++ b/rhino/layer_builder.py @@ -0,0 +1,436 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +layer_builder.py +Layer-Struktur: + + +-- 00_RASTER + +-- 01_VERMESSUNG + +-- 20_WAENDE + +-- ... +Jede Zeichnungsebene erhaelt alle definierten Ebenen als Sublayer. +""" +import System +import System.Drawing as Drawing +import Rhino + +GREY = Drawing.Color.FromArgb(150, 150, 150) +_FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer +_FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject +_EMPTY_GUID = System.Guid.Empty + + +def _color(hex_str): + h = (hex_str or "#888888").lstrip("#") + if len(h) == 3: + h = "".join(c * 2 for c in h) + return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) + + +def _is_top_level(layer): + return layer.ParentLayerId == _EMPTY_GUID + + +def _find_top_by_id(doc, dossier_id): + for i, layer in enumerate(doc.Layers): + if not _is_top_level(layer): + continue + v = layer.GetUserString("dossier_id") + if v == dossier_id: + return i + return -1 + + +def _find_top_by_name(doc, name): + for i, layer in enumerate(doc.Layers): + if _is_top_level(layer) and layer.Name == name: + return i + return -1 + + +def _find_sublayer_by_code(doc, parent_id, code): + prefix = code + "_" + for i, layer in enumerate(doc.Layers): + if layer.ParentLayerId == parent_id and layer.Name.startswith(prefix): + return i + return -1 + + +def _add_layer(doc, name, parent_id=None, color=None, lw=None): + layer = Rhino.DocObjects.Layer() + layer.Name = name + if parent_id is not None and parent_id != _EMPTY_GUID: + layer.ParentLayerId = parent_id + if color is not None: + layer.Color = color + if lw is not None: + layer.PlotWeight = lw + return doc.Layers.Add(layer) + + +def build_layers(doc, zeichnungsebenen, ebenen): + """ + Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert + und unter jedem alle Ebenen als Sublayer angelegt/aktualisiert sind. + """ + for z in zeichnungsebenen: + z_id = z["id"] + z_name = z["name"] + + # Parent finden oder anlegen + idx = _find_top_by_id(doc, z_id) + if idx < 0: + idx = _find_top_by_name(doc, z_name) + if idx < 0: + idx = _add_layer(doc, z_name) + doc.Layers[idx].SetUserString("dossier_id", z_id) + else: + parent = doc.Layers[idx] + if parent.Name != z_name: + parent.Name = z_name + parent.SetUserString("dossier_id", z_id) + + parent_id = doc.Layers[idx].Id + + # Sublayer pro Ebene + for e in ebenen: + sub_name = "{}_{}".format(e["code"], e["name"]) + col = _color(e.get("color")) + lw = float(e.get("lw", 0.13)) + sub_idx = _find_sublayer_by_code(doc, parent_id, e["code"]) + if sub_idx < 0: + sub_idx = _add_layer(doc, sub_name, parent_id, col, lw) + doc.Layers[sub_idx].SetUserString("dossier_code", e["code"]) + else: + sub = doc.Layers[sub_idx] + if sub.Name != sub_name: + sub.Name = sub_name + sub.Color = col + try: + import massstab as _ms + _ms.write_plotweight(doc, sub, float(lw)) + except Exception: + sub.PlotWeight = lw + sub.SetUserString("dossier_code", e["code"]) + + doc.Views.Redraw() + print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert".format( + len(zeichnungsebenen), len(ebenen))) + + +def update_layer_style(doc, code, color_hex=None, lw=None): + """Aendert Farbe und/oder Stiftdicke fuer alle Sublayer mit dem gegebenen Code.""" + col = _color(color_hex) if color_hex else None + try: + import massstab as _ms + except Exception: + _ms = None + for i, layer in enumerate(doc.Layers): + if _is_top_level(layer): + continue + if layer.Name.startswith(code + "_"): + if col is not None: + layer.Color = col + if lw is not None: + if _ms is not None: + _ms.write_plotweight(doc, layer, float(lw)) + else: + layer.PlotWeight = float(lw) + doc.Views.Redraw() + + +def set_ebene_visible(doc, code, visible): + """Schaltet alle Sublayer mit Code in/aus Zeichnungsebenen.""" + for i, layer in enumerate(doc.Layers): + if _is_top_level(layer): + continue + if layer.Name.startswith(code + "_"): + layer.IsVisible = visible + doc.Views.Redraw() + + +def set_ebene_locked(doc, code, locked): + for i, layer in enumerate(doc.Layers): + if _is_top_level(layer): + continue + if layer.Name.startswith(code + "_"): + layer.IsLocked = locked + doc.Views.Redraw() + + +def delete_ebene(doc, code, move_to=None): + """ + Loescht alle Sublayer mit dem gegebenen Code in allen Zeichnungsebenen. + Falls move_to gesetzt: verschiebt vorher alle Objekte zum Sublayer + mit move_to-Code unter dem selben Parent. Sonst: loescht Objekte mit. + """ + if not code: + return + + from_prefix = code + "_" + to_prefix = (move_to + "_") if move_to else None + + # Top-Level Parents finden + parents = [layer for layer in doc.Layers if _is_top_level(layer)] + + moved = 0 + deleted_objs = 0 + deleted_layers = 0 + + for parent in parents: + from_layer = None + to_layer = None + for layer in doc.Layers: + if layer.ParentLayerId != parent.Id: + continue + if layer.Name.startswith(from_prefix): + from_layer = layer + elif to_prefix and layer.Name.startswith(to_prefix): + to_layer = layer + + if from_layer is None: + continue + + from_idx = doc.Layers.FindByFullPath(from_layer.FullPath, -1) + if from_idx < 0: + continue + + objs = list(doc.Objects.FindByLayer(doc.Layers[from_idx])) + + if move_to and to_layer is not None: + to_idx = doc.Layers.FindByFullPath(to_layer.FullPath, -1) + if to_idx >= 0: + for obj in objs: + attrs = obj.Attributes.Duplicate() + attrs.LayerIndex = to_idx + if doc.Objects.ModifyAttributes(obj, attrs, True): + moved += 1 + else: + for obj in objs: + if doc.Objects.Delete(obj.Id, True): + deleted_objs += 1 + + # Sublayer loeschen + try: + if doc.Layers.Delete(from_idx, True): + deleted_layers += 1 + except Exception as ex: + print("[EBENEN] Layer-Delete:", ex) + + doc.Views.Redraw() + print("[EBENEN] Ebene {} entfernt: {} Sublayer, {} Objekte verschoben, {} Objekte geloescht".format( + code, deleted_layers, moved, deleted_objs)) + + +# --- Clipping Plane Management ---------------------------------------------- + +_CLIP_KEY = "dossier_clipping_plane" + + +def _find_clipping_plane(doc): + for obj in doc.Objects: + try: + if obj.Attributes.GetUserString(_CLIP_KEY) == "1": + return obj + except Exception: + pass + return None + + +def update_clipping_plane(doc, active_z, enabled): + """ + Erstellt/aktualisiert/entfernt die DOSSIER-Clipping-Plane an OKFF + Schnitthoehe + des aktiven Geschosses. Plane zeigt nach +Z, schneidet alles oberhalb weg. + """ + import Rhino.Geometry as rg + existing = _find_clipping_plane(doc) + 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): + if existing is not None: + try: + doc.Objects.Delete(existing.Id, True) + except Exception: + 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: + print("[EBENEN] Clip-Update:", ex) + else: + vp_ids = [] + for view in doc.Views: + try: + vp_ids.append(view.ActiveViewportID) + except Exception: + try: vp_ids.append(view.ActiveViewport.Id) + except Exception: pass + try: + new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids) + if new_id != System.Guid.Empty: + obj = doc.Objects.FindId(new_id) + if obj is not None: + attrs = obj.Attributes.Duplicate() + attrs.SetUserString(_CLIP_KEY, "1") + attrs.Mode = Rhino.DocObjects.ObjectMode.Locked + doc.Objects.ModifyAttributes(obj, attrs, True) + print("[EBENEN] Clipping-Plane bei Z={} erstellt".format(cut_z)) + except Exception as ex: + print("[EBENEN] Clip-Create:", ex) + doc.Views.Redraw() + + +def cleanup_default_layers(doc): + """Loescht leere Rhino-Default-Layer (Default, Layer 01, ...) — nicht-leere bleiben unberuehrt.""" + import re + pattern = re.compile(r'^(default|layer\s*0*\d+)$', re.IGNORECASE) + deleted = [] + for i in range(doc.Layers.Count - 1, -1, -1): + layer = doc.Layers[i] + if layer.IsDeleted: + continue + if not _is_top_level(layer): + continue + if not pattern.match(layer.Name.strip()): + continue + try: + # Name VOR Delete sichern — sonst liefert layer.Name danach None + nm = layer.Name + if doc.Layers.Delete(i, True): + if nm: deleted.append(nm) + except Exception: + pass + if deleted: + print("[EBENEN] Default-Layer entfernt: {}".format(", ".join(deleted))) + + +def set_active_sublayer(doc, zeichnungsebene_id, code): + """Macht den Sublayer 'code' unter Zeichnungsebene 'zeichnungsebene_id' aktiv.""" + parent_idx = _find_top_by_id(doc, zeichnungsebene_id) + if parent_idx < 0: + print("[EBENEN] Parent-Layer fuer Zeichnungsebene {} nicht gefunden".format(zeichnungsebene_id)) + return + parent_id = doc.Layers[parent_idx].Id + sub_idx = _find_sublayer_by_code(doc, parent_id, code) + if sub_idx >= 0: + doc.Layers.SetCurrentLayerIndex(sub_idx, True) + else: + print("[EBENEN] Sublayer mit Code {} unter Parent {} nicht gefunden".format(code, doc.Layers[parent_idx].Name)) + + +def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_mode, e_mode): + """ + Kombinierte Sichtbarkeit aus Z-Mode (Zeichnungsebenen) und E-Mode (Ebenen). + Beide Modi: 'all' | 'active' | 'grey' | 'grey_locked' + """ + canonical = {e["code"]: _color(e.get("color")) for e in ebenen} + e_eye_vis = {e["code"]: e.get("visible", True) for e in ebenen} + e_eye_locked = {e["code"]: e.get("locked", False) for e in ebenen} + + id_to_top, name_to_top, children_by_parent = {}, {}, {} + for layer in doc.Layers: + if _is_top_level(layer): + uid = layer.GetUserString("dossier_id") + if uid: + id_to_top[uid] = layer + name_to_top[layer.Name] = layer + else: + children_by_parent.setdefault(layer.ParentLayerId, []).append(layer) + + for z in zeichnungsebenen: + parent = id_to_top.get(z["id"]) or name_to_top.get(z["name"]) + if parent is None: + continue + children = children_by_parent.get(parent.Id, []) + is_active_z = z["id"] == active_z_id + z_visible_flag = z.get("visible", True) + + # Z-Mode -> Parent-Zustand + if is_active_z: + p_vis, p_grey, p_lock = True, False, False + elif z_mode == "active": + p_vis, p_grey, p_lock = False, False, False + elif not z_visible_flag: + p_vis, p_grey, p_lock = False, False, False + elif z_mode == "all": + p_vis, p_grey, p_lock = True, False, False + elif z_mode == "grey_locked": + p_vis, p_grey, p_lock = True, True, True + else: # grey + p_vis, p_grey, p_lock = True, True, False + + parent_changed = False + if parent.IsVisible != p_vis: + parent.IsVisible = p_vis + parent_changed = True + if parent.IsLocked != p_lock: + parent.IsLocked = p_lock + parent_changed = True + if parent_changed: + try: doc.Layers.Modify(parent, parent.LayerIndex, True) + except Exception: pass + + if not p_vis: + continue # Children erben Parent-Hidden + + # E-Mode -> Sublayer-Zustand + for child in children: + if "_" not in child.Name: + continue + code = child.Name.split("_", 1)[0] + if code not in canonical: + continue + is_active_e = (code == active_code) + eye_v = e_eye_vis.get(code, True) + eye_l = e_eye_locked.get(code, False) + + if is_active_e: + e_vis, e_grey, e_lock = True, False, False + elif e_mode == "active": + e_vis, e_grey, e_lock = False, False, False + elif not eye_v: + e_vis, e_grey, e_lock = False, False, False + elif e_mode == "all": + e_vis, e_grey, e_lock = True, False, False + elif e_mode == "grey_locked": + e_vis, e_grey, e_lock = True, True, True + else: # grey + e_vis, e_grey, e_lock = True, True, False + + # Kombination + child_vis = e_vis + child_grey = p_grey or e_grey + child_lock = e_lock or eye_l + + changed = False + if child.IsVisible != child_vis: + child.IsVisible = child_vis + changed = True + if child.IsLocked != child_lock: + child.IsLocked = child_lock + changed = True + if child_grey: + if child.Color != GREY: + child.Color = GREY + changed = True + else: + canon = canonical.get(code) + if canon is not None and child.Color != canon: + child.Color = canon + changed = True + # In neueren Rhino-Versionen committed der Property-Setter direkt, + # in manchen Faellen (besonders auf Mac) wird IsLocked nicht + # persistiert ohne explizites Modify. Defensiv: + if changed: + try: + doc.Layers.Modify(child, child.LayerIndex, True) + except Exception: + pass + + doc.Views.Redraw() diff --git a/rhino/layouts.py b/rhino/layouts.py new file mode 100644 index 0000000..fe188fd --- /dev/null +++ b/rhino/layouts.py @@ -0,0 +1,748 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +layouts.py +LAYOUTS-Panel: Layout-Pages erstellen + Details mit Ausschnitten bestuecken. +Phase 1 — Snapshot-Mode: Ausschnitt wird beim Zuweisen auf das Detail angewendet, +Re-Sync per Knopf. Live-Link und Masterlayouts kommen spaeter. +""" +import os +import sys +import json +import Rhino +import Rhino.Geometry as rg +import System +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import panel_base + +PANEL_GUID_STR = "4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1" + +# UserString-Key auf jedem Detail — speichert die Ausschnitt-Bindung. +_BIND_KEY = "dossier_bound_ausschnitt" +# Doc-Strings fuer Layout-Folder-Organisation (analog Ausschnitte-Ordner). +_FOLDER_LIST_KEY = "dossier_layout_folders" # JSON-Array: ["A","B",...] +_FOLDER_MAP_KEY = "dossier_layout_folder_map" # JSON-Dict: {pageId: "A"} + +# Vordefinierte Papierformate in Millimetern (Welt-Einheit wird beim Erstellen +# umgerechnet, falls das Doc nicht auf mm steht). +PAPER_SIZES_MM = { + "A0": (841, 1189), + "A1": (594, 841), + "A2": (420, 594), + "A3": (297, 420), + "A4": (210, 297), + "Letter": (216, 279), +} + + +def _page_unit_system(doc): + """Rhino hat ein eigenes Unit-System fuer Layouts (PageUnitSystem), das + sich vom Modell-Unit-System unterscheiden kann (z.B. Modell in Meter, + Plan in Millimeter — Standard bei Architektur). AddPageView und + SetPageSize erwarten Werte in PageUnitSystem, NICHT in ModelUnitSystem.""" + try: + return doc.PageUnitSystem + except Exception: + return doc.ModelUnitSystem + + +def _mm_to_page(doc): + """Faktor: mm -> Page-Unit. Wird fuer AddPageView/SetPageSize benutzt.""" + try: + return Rhino.RhinoMath.UnitScale(Rhino.UnitSystem.Millimeters, + _page_unit_system(doc)) + except Exception: + return 1.0 + + +def _page_to_mm(doc): + """Faktor: Page-Unit -> mm. Wird beim PDF-Export gebraucht.""" + try: + return Rhino.RhinoMath.UnitScale(_page_unit_system(doc), + Rhino.UnitSystem.Millimeters) + except Exception: + return 1.0 + + +def _page_size_in_doc(doc, fmt, landscape): + """Liefert (width, height) in Page-Units.""" + if fmt not in PAPER_SIZES_MM: return None + w_mm, h_mm = PAPER_SIZES_MM[fmt] + if landscape: + w_mm, h_mm = h_mm, w_mm + f = _mm_to_page(doc) + return (w_mm * f, h_mm * f) + + +def _load_folder_list(doc): + """Liefert die Liste explizit angelegter Ordner (Reihenfolge bleibt).""" + raw = doc.Strings.GetValue(_FOLDER_LIST_KEY) + if not raw: return [] + try: + data = json.loads(raw) + return [n for n in data if isinstance(n, str) and n] + except Exception: + return [] + + +def _save_folder_list(doc, names): + try: + doc.Strings.SetString(_FOLDER_LIST_KEY, json.dumps(list(names), ensure_ascii=False)) + except Exception as ex: + print("[LAYOUTS] _save_folder_list:", ex) + + +def _load_folder_map(doc): + """page_id -> folder_name.""" + raw = doc.Strings.GetValue(_FOLDER_MAP_KEY) + if not raw: return {} + try: + data = json.loads(raw) + if isinstance(data, dict): + return {k: v for k, v in data.items() if isinstance(v, str) and v} + except Exception: + pass + return {} + + +def _save_folder_map(doc, m): + try: + doc.Strings.SetString(_FOLDER_MAP_KEY, json.dumps(m, ensure_ascii=False)) + except Exception as ex: + print("[LAYOUTS] _save_folder_map:", ex) + + +def _list_layouts(doc): + """Liefert dict-Liste aller PageView-Layouts inkl. Ordner-Zuweisung. + Groessen werden ZUSAETZLICH in mm geliefert, damit das Frontend ohne + Unit-Kenntnis formatieren kann.""" + fmap = _load_folder_map(doc) + pu_to_mm = _page_to_mm(doc) + out = [] + for v in doc.Views: + if isinstance(v, Rhino.Display.RhinoPageView): + try: + pid = str(v.MainViewport.Id) + w = float(v.PageWidth) + h = float(v.PageHeight) + out.append({ + "id": pid, + "name": v.PageName or "", + "width": w, + "height": h, + "widthMm": w * pu_to_mm, + "heightMm": h * pu_to_mm, + "detailCount": len(v.GetDetailViews() or []), + "folder": fmap.get(pid, ""), + }) + except Exception as ex: + print("[LAYOUTS] list_layouts:", ex) + return out + + +def _find_page_by_id(doc, page_id): + """page_id ist die MainViewport.Id (str). Liefert RhinoPageView oder None.""" + for v in doc.Views: + if isinstance(v, Rhino.Display.RhinoPageView): + try: + if str(v.MainViewport.Id) == page_id: + return v + except Exception: + pass + return None + + +def _find_detail_by_id(doc, page, detail_id): + """Detail-Object auf einer Page anhand seiner Detail-ID.""" + try: + for d in page.GetDetailViews(): + if str(d.Id) == detail_id: + return d + except Exception as ex: + print("[LAYOUTS] find_detail:", ex) + return None + + +def _get_detail_binding(detail): + """Liest gebundene Ausschnitt-ID aus dem Detail-UserString.""" + try: + v = detail.Attributes.GetUserString(_BIND_KEY) + return v if v else None + except Exception: + return None + + +def _set_detail_binding(detail, snap_id): + """Schreibt/loescht die Ausschnitt-ID auf das Detail-UserString.""" + try: + if snap_id: + detail.Attributes.SetUserString(_BIND_KEY, snap_id) + else: + detail.Attributes.SetUserString(_BIND_KEY, "") # leerer String = "kein Binding" + detail.CommitChanges() + return True + except Exception as ex: + print("[LAYOUTS] set_binding:", ex) + return False + + +def _detail_dict(detail, snap_lookup): + """Serialisiert ein Detail fuer das Frontend.""" + bbox = detail.Geometry.GetBoundingBox(True) # in Welt-Koordinaten der Page + bound_id = _get_detail_binding(detail) + bound_name = (snap_lookup.get(bound_id, {}) or {}).get("name") if bound_id else None + try: + vp_name = detail.Viewport.Name + except Exception: + vp_name = "" + return { + "id": str(detail.Id), + "name": vp_name, + "x": float(bbox.Min.X), + "y": float(bbox.Min.Y), + "width": float(bbox.Max.X - bbox.Min.X), + "height": float(bbox.Max.Y - bbox.Min.Y), + "boundAusschnitt": bound_id, + "boundAusschnittName": bound_name, + } + + +def _snap_lookup(doc): + """Map snap_id -> snap dict. Wird fuer Detail-Display gebraucht.""" + out = {} + try: + raw = doc.Strings.GetValue("dossier_ausschnitte") + if raw: + data = json.loads(raw) + if isinstance(data, list): + for s in data: + if isinstance(s, dict) and s.get("id"): + out[s["id"]] = s + except Exception: + pass + return out + + +def _slim_snaps(doc): + """Schlanke Liste von Snapshots fuer Frontend-Dropdown.""" + out = [] + try: + raw = doc.Strings.GetValue("dossier_ausschnitte") + if not raw: return [] + data = json.loads(raw) + if not isinstance(data, list): return [] + for s in data: + if isinstance(s, dict) and s.get("id"): + out.append({ + "id": s.get("id"), + "name": s.get("name"), + "folder": s.get("folder", ""), + "scale": s.get("scale", ""), + }) + except Exception: + pass + return out + + +# --- Bridge ----------------------------------------------------------------- + +class LayoutsBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "layouts") + + def _on_ready(self): + self._send_state() + + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): p = {} + + if t == "READY": self._on_ready() + elif t == "LIST": self._send_state() + elif t == "NEW_LAYOUT": self._new_layout(p) + elif t == "DELETE_LAYOUT": self._delete_layout(p.get("id")) + elif t == "RENAME_LAYOUT": self._rename_layout(p.get("id"), p.get("name")) + elif t == "SET_PAGE_SIZE": self._set_page_size(p) + elif t == "ACTIVATE_LAYOUT": self._activate_layout(p.get("id")) + elif t == "EXPORT_PDF": self._export_pdf(p) + elif t == "ADD_DETAIL": self._add_detail(p) + elif t == "DELETE_DETAIL": self._delete_detail(p.get("pageId"), p.get("detailId")) + elif t == "BIND_AUSSCHNITT": self._bind_ausschnitt(p) + elif t == "SYNC_DETAIL": self._sync_detail(p.get("pageId"), p.get("detailId")) + elif t == "SYNC_LAYOUT": self._sync_layout(p.get("id")) + # Ordner-Management + elif t == "ADD_FOLDER": self._add_folder(p.get("name")) + elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name")) + elif t == "SET_FOLDER": self._set_folder(p.get("id"), p.get("folder") or "") + + # --- State-Snapshot ----------------------------------------------------- + + def _send_state(self): + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: + self.send("STATE", {"layouts": [], "snapshots": [], "details": {}, "folders": []}) + return + layouts = _list_layouts(doc) + snaps = _slim_snaps(doc) + snap_lookup = _snap_lookup(doc) + # Ordner: explizite Liste + alle in Layouts referenzierten + explicit_folders = _load_folder_list(doc) + for l in layouts: + f = l.get("folder") + if f and f not in explicit_folders: + explicit_folders.append(f) + # Pro Layout die Details mitgeben + details = {} + for v in doc.Views: + if isinstance(v, Rhino.Display.RhinoPageView): + try: + pid = str(v.MainViewport.Id) + details[pid] = [_detail_dict(d, snap_lookup) for d in v.GetDetailViews()] + except Exception as ex: + print("[LAYOUTS] details for page:", ex) + self.send("STATE", { + "layouts": layouts, + "snapshots": snaps, + "details": details, + "folders": explicit_folders, + }) + + # --- Ordner ------------------------------------------------------------- + + def _add_folder(self, name): + if not name: return + name = name.strip() + if not name: return + doc = Rhino.RhinoDoc.ActiveDoc + folders = _load_folder_list(doc) + if name not in folders: + folders.append(name) + _save_folder_list(doc, folders) + self._send_state() + + def _remove_folder(self, name): + if not name: return + doc = Rhino.RhinoDoc.ActiveDoc + folders = [f for f in _load_folder_list(doc) if f != name] + _save_folder_list(doc, folders) + # Layouts aus diesem Ordner herausnehmen (zurueck auf Root) + m = _load_folder_map(doc) + m = {k: v for k, v in m.items() if v != name} + _save_folder_map(doc, m) + self._send_state() + + def _set_folder(self, page_id, folder): + if not page_id: return + doc = Rhino.RhinoDoc.ActiveDoc + m = _load_folder_map(doc) + if folder: + m[page_id] = folder + # Sicherstellen dass der Ordner-Name in der expliziten Liste ist + folders = _load_folder_list(doc) + if folder not in folders: + folders.append(folder) + _save_folder_list(doc, folders) + else: + if page_id in m: del m[page_id] + _save_folder_map(doc, m) + self._send_state() + + # --- Layouts ------------------------------------------------------------ + + def _resolve_size(self, doc, p): + """Bestimmt (w, h) in Page-Units aus Payload — Format-Name ODER + customWidth/customHeight in mm.""" + fmt = p.get("format") + if fmt == "custom": + try: + wmm = float(p.get("customWidth")) + hmm = float(p.get("customHeight")) + except Exception: + return None + if wmm <= 0 or hmm <= 0: return None + f = _mm_to_page(doc) + return (wmm * f, hmm * f) + if fmt in PAPER_SIZES_MM: + return _page_size_in_doc(doc, fmt, bool(p.get("landscape", True))) + return None + + def _new_layout(self, p): + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + name = (p.get("name") or "").strip() + size = self._resolve_size(doc, p) + if size is None: + print("[LAYOUTS] ungueltige Groesse:", p); return + w, h = size + if not name: + name = "Layout {}".format(len(_list_layouts(doc)) + 1) + try: + page = doc.Views.AddPageView(name, w, h) + if page is None: + print("[LAYOUTS] AddPageView fehlgeschlagen"); return + print("[LAYOUTS] '{}' angelegt ({}x{})".format(name, w, h)) + except Exception as ex: + print("[LAYOUTS] AddPageView Fehler:", ex) + self._send_state() + + def _set_page_size(self, p): + """Aendert die Groesse einer bestehenden Layout-Seite via + RhinoPageView.SetPageSize (Rhino 8). Faellt zurueck auf Property- + Setter, falls die Methode nicht existiert.""" + doc = Rhino.RhinoDoc.ActiveDoc + page = _find_page_by_id(doc, p.get("id")) + if page is None: return + size = self._resolve_size(doc, p) + if size is None: + print("[LAYOUTS] ungueltige Groesse fuer set_page_size:", p); return + w, h = size + done = False + # 1) SetPageSize(double, double) — Rhino 8 + if hasattr(page, "SetPageSize"): + try: + page.SetPageSize(float(w), float(h)) + done = True + print("[LAYOUTS] SetPageSize -> {}x{}".format(w, h)) + except Exception as ex: + print("[LAYOUTS] SetPageSize fehlgeschlagen:", ex) + # 2) Fallback: Properties (haengt von Rhino-Version ab) + if not done: + try: + page.PageWidth = float(w) + page.PageHeight = float(h) + done = True + print("[LAYOUTS] PageWidth/Height-Properties -> {}x{}".format(w, h)) + except Exception as ex: + print("[LAYOUTS] Property-Setter fehlgeschlagen:", ex) + if not done: + print("[LAYOUTS] Konnte Seiten-Groesse nicht setzen — bitte ueber Rhinos Layout-Dialog aendern") + try: page.Redraw() + except Exception: pass + self._send_state() + + def _export_pdf(self, p): + """Exportiert Layouts als ein gemeinsames PDF. Akzeptiert: + - "ids": Liste von Layout-IDs (Multi-Export) + - "id": einzelne Layout-ID + - sonst alle Layouts. + Save-Dialog via Eto.Forms, Inhalt via FilePdf API mit + EXPLIZITER Pixel-Groesse aus Page-Dimensionen (sonst wird's ein + Mini-Bildchen).""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + ids = p.get("ids") + if not ids and p.get("id"): ids = [p.get("id")] + dpi = float(p.get("dpi") or 300) + targets = [] + if ids: + for pid in ids: + page = _find_page_by_id(doc, pid) + if page is not None: targets.append(page) + else: + for v in doc.Views: + if isinstance(v, Rhino.Display.RhinoPageView): + targets.append(v) + if not targets: + print("[LAYOUTS] kein Layout zu exportieren"); return + path = self._pick_save_path(doc, targets) + if not path: + print("[LAYOUTS] Export abgebrochen") + return + # PageWidth/PageHeight sind in Page-Units — fuer die Pixel-Berechnung + # rechnen wir in mm um (1 inch = 25.4 mm). + mm_per_pu = _page_to_mm(doc) + # Remember current active view to restore afterwards + prev_view = None + try: prev_view = doc.Views.ActiveView + except Exception: pass + try: + pdf = Rhino.FileIO.FilePdf.Create() + n_added = 0 + for page in targets: + try: + # Page muss kurz aktiv sein — sonst rendert ViewCapture + # leer (vor allem auf macOS). Kurzer Idle-Wait gibt dem + # Renderer Zeit, sonst kommt's beim ERSTEN Page-Wechsel + # zu Race-Bedingungen. + try: + page.SetPageAsActive() + doc.Views.ActiveView = page + page.Redraw() + doc.Views.Redraw() + Rhino.RhinoApp.Wait() # einen Idle-Tick verarbeiten + except Exception: pass + # Page-Size in mm -> Pixel @dpi + w_mm = float(page.PageWidth) * mm_per_pu + h_mm = float(page.PageHeight) * mm_per_pu + if w_mm <= 0 or h_mm <= 0: + print("[LAYOUTS] Page '{}' hat ungueltige Groesse: {}x{} mm".format( + page.PageName, w_mm, h_mm)) + continue + px_w = max(1, int(round(w_mm / 25.4 * dpi))) + px_h = max(1, int(round(h_mm / 25.4 * dpi))) + # Settings mit expliziter Groesse — die Drei-Argument- + # Variante ist die zuverlaessige fuer PDF-Export. + settings = None + try: + size = System.Drawing.Size(px_w, px_h) + settings = Rhino.Display.ViewCaptureSettings(page, size, dpi) + except Exception as ex: + print("[LAYOUTS] 3-arg Settings:", ex) + if settings is None: + settings = Rhino.Display.ViewCaptureSettings(page, dpi) + # Vector-Output — sonst wird's gerastert und klein + try: settings.RasterMode = False + except Exception: pass + pdf.AddPage(settings) + n_added += 1 + print("[LAYOUTS] add page '{}': {}x{}mm -> {}x{}px".format( + page.PageName, w_mm, h_mm, px_w, px_h)) + except Exception as ex: + print("[LAYOUTS] add_page '{}': {}".format(page.PageName, ex)) + if n_added == 0: + print("[LAYOUTS] Keine Seiten konnten hinzugefuegt werden") + else: + pdf.Write(path) + print("[LAYOUTS] PDF geschrieben: {} ({} Seite(n))".format(path, n_added)) + except Exception as ex: + print("[LAYOUTS] PDF-Export fehlgeschlagen:", ex) + finally: + # Vorherige View wieder aktivieren + if prev_view is not None: + try: doc.Views.ActiveView = prev_view + except Exception: pass + + def _pick_save_path(self, doc, targets): + """Eto.Forms SaveFileDialog — Default: Doc-Folder + erster Layout-Name.""" + try: + import Eto.Forms as forms + dlg = forms.SaveFileDialog() + dlg.Filters.Add(forms.FileFilter("PDF", ".pdf")) + try: dlg.CurrentFilterIndex = 0 + except Exception: pass + # Default-Filename — Layout-Name oder Doc-Name + if len(targets) == 1: + base = targets[0].PageName or "Layout" + else: + base = "Layouts" + if doc.Path: + base = os.path.splitext(os.path.basename(doc.Path))[0] + "_Layouts" + dlg.FileName = "{}.pdf".format(base) + # Default-Folder — neben der .3dm wenn vorhanden + if doc.Path: + try: dlg.Directory = System.Uri(os.path.dirname(doc.Path)) + except Exception: pass + try: + import Rhino.UI as RhinoUI + parent = RhinoUI.RhinoEtoApp.MainWindow + except Exception: + parent = None + result = dlg.ShowDialog(parent) + if str(result) != "Ok": + return None + path = dlg.FileName + if path and not path.lower().endswith(".pdf"): + path += ".pdf" + return path + except Exception as ex: + print("[LAYOUTS] SaveFileDialog:", ex) + return None + + def _delete_layout(self, page_id): + doc = Rhino.RhinoDoc.ActiveDoc + page = _find_page_by_id(doc, page_id) + if page is None: + print("[LAYOUTS] delete: page not found", page_id); return + name = page.PageName + # Andere View aktivieren — Rhino verweigert oft, die aktive Page zu loeschen + try: + for v in doc.Views: + if v is not page and not isinstance(v, Rhino.Display.RhinoPageView): + doc.Views.ActiveView = v + break + else: + # Nur PageViews da — irgendeine andere aktivieren + for v in doc.Views: + if v is not page: + doc.Views.ActiveView = v + break + except Exception as ex: + print("[LAYOUTS] activate other view:", ex) + done = False + # Methode 1: doc.Views.Remove + try: + r = doc.Views.Remove(page) + print("[LAYOUTS] doc.Views.Remove returned:", r) + # Verify + if _find_page_by_id(doc, page_id) is None: + done = True + except Exception as ex: + print("[LAYOUTS] doc.Views.Remove failed:", ex) + # Methode 2: Close + Delete via RunScript-Fallback + if not done: + try: + Rhino.RhinoApp.RunScript('_-CommandHistory _Hide _Enter', False) + except Exception: pass + try: + # Rhino 8 hat _-Layout _Delete + Rhino.RhinoApp.RunScript('_-Layout _Delete "{}" _Enter'.format(name), False) + if _find_page_by_id(doc, page_id) is None: + done = True + print("[LAYOUTS] geloescht via _-Layout _Delete") + except Exception as ex: + print("[LAYOUTS] _-Layout _Delete failed:", ex) + if not done: + print("[LAYOUTS] Konnte Layout '{}' nicht loeschen — bitte manuell ueber Layout-Tab".format(name)) + # Folder-Mapping aufraeumen + try: + m = _load_folder_map(doc) + if page_id in m: + del m[page_id] + _save_folder_map(doc, m) + except Exception: pass + self._send_state() + + def _rename_layout(self, page_id, name): + doc = Rhino.RhinoDoc.ActiveDoc + page = _find_page_by_id(doc, page_id) + if page is None or not name: return + try: + page.PageName = name.strip() + except Exception as ex: + print("[LAYOUTS] Rename page:", ex) + self._send_state() + + def _activate_layout(self, page_id): + doc = Rhino.RhinoDoc.ActiveDoc + page = _find_page_by_id(doc, page_id) + if page is None: return + try: + page.SetPageAsActive() + doc.Views.ActiveView = page + except Exception as ex: + print("[LAYOUTS] activate page:", ex) + + # --- Details ------------------------------------------------------------ + + def _add_detail(self, p): + """Neues Detail auf einer Seite anlegen. Optional gleich an einen + Ausschnitt binden (= Snapshot anwenden).""" + doc = Rhino.RhinoDoc.ActiveDoc + page = _find_page_by_id(doc, p.get("pageId")) + if page is None: return + # Bounding-Box auf der Seite: default 80% der Seitenflaeche, zentriert + try: + pw = page.PageWidth + ph = page.PageHeight + margin_x = pw * 0.1 + margin_y = ph * 0.1 + c0 = rg.Point2d(margin_x, margin_y) + c1 = rg.Point2d(pw - margin_x, ph - margin_y) + # Erlaubt Override per Payload + for k_min, default in (("x", margin_x), ("y", margin_y)): + v = p.get(k_min) + if isinstance(v, (int, float)): + if k_min == "x": c0 = rg.Point2d(float(v), c0.Y) + if k_min == "y": c0 = rg.Point2d(c0.X, float(v)) + for k_max, default in (("x2", pw - margin_x), ("y2", ph - margin_y)): + v = p.get(k_max) + if isinstance(v, (int, float)): + if k_max == "x2": c1 = rg.Point2d(float(v), c1.Y) + if k_max == "y2": c1 = rg.Point2d(c1.X, float(v)) + proj = Rhino.Display.DefinedViewportProjection.Top + detail = page.AddDetailView("Detail", c0, c1, proj) + if detail is None: + print("[LAYOUTS] AddDetailView gab None"); return + page.Redraw() + # Optional Ausschnitt binden + anwenden + snap_id = p.get("ausschnittId") + if snap_id: + _set_detail_binding(detail, snap_id) + try: + import ausschnitte + ausschnitte.apply_snapshot_to_detail(doc, detail, snap_id) + except Exception as ex: + print("[LAYOUTS] initial apply:", ex) + except Exception as ex: + print("[LAYOUTS] AddDetailView:", ex) + self._send_state() + + def _delete_detail(self, page_id, detail_id): + doc = Rhino.RhinoDoc.ActiveDoc + page = _find_page_by_id(doc, page_id) + if page is None: return + detail = _find_detail_by_id(doc, page, detail_id) + if detail is None: return + try: + doc.Objects.Delete(detail.Id, True) + page.Redraw() + except Exception as ex: + print("[LAYOUTS] Delete detail:", ex) + self._send_state() + + def _bind_ausschnitt(self, p): + """Setzt die Binding und wendet den Ausschnitt sofort an (Snapshot-Mode).""" + doc = Rhino.RhinoDoc.ActiveDoc + page = _find_page_by_id(doc, p.get("pageId")) + if page is None: return + detail = _find_detail_by_id(doc, page, p.get("detailId")) + if detail is None: return + snap_id = p.get("ausschnittId") or None + _set_detail_binding(detail, snap_id) + if snap_id: + try: + import ausschnitte + ausschnitte.apply_snapshot_to_detail(doc, detail, snap_id) + except Exception as ex: + print("[LAYOUTS] apply on bind:", ex) + self._send_state() + + def _sync_detail(self, page_id, detail_id): + doc = Rhino.RhinoDoc.ActiveDoc + page = _find_page_by_id(doc, page_id) + if page is None: return + detail = _find_detail_by_id(doc, page, detail_id) + if detail is None: return + snap_id = _get_detail_binding(detail) + if not snap_id: + print("[LAYOUTS] sync: kein Binding auf diesem Detail") + return + try: + import ausschnitte + ausschnitte.apply_snapshot_to_detail(doc, detail, snap_id) + except Exception as ex: + print("[LAYOUTS] sync:", ex) + self._send_state() + + def _sync_layout(self, page_id): + """Alle Details der Page mit ihren Bindings re-applien.""" + doc = Rhino.RhinoDoc.ActiveDoc + page = _find_page_by_id(doc, page_id) + if page is None: return + try: + import ausschnitte + for d in page.GetDetailViews(): + snap_id = _get_detail_binding(d) + if snap_id: + ausschnitte.apply_snapshot_to_detail(doc, d, snap_id) + page.Redraw() + except Exception as ex: + print("[LAYOUTS] sync layout:", ex) + self._send_state() + + +def _bridge_factory(): + b = LayoutsBridge() + sc.sticky["layouts_bridge"] = b + return b + + +panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR, + _bridge_factory, icon_spec=("L", "#7a5fa8")) diff --git a/rhino/massstab.py b/rhino/massstab.py new file mode 100644 index 0000000..7bbf1fc --- /dev/null +++ b/rhino/massstab.py @@ -0,0 +1,1096 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +massstab.py +MASSSTAB-Panel: zeigt + setzt den aktuellen Massstab des aktiven Viewports. +Funktioniert nur in Parallelprojektion. In Perspective wird "—" gemeldet. + +Maesstabs-Mathematik: + frustum_width_in_doc_units = vp.GetFrustum() -> (right - left) + frustum_width_mm = frustum_width_in_doc_units * mm_per_doc_unit + screen_width_mm = pixel_width * 25.4 / dpi + N = frustum_width_mm / screen_width_mm (Skala 1:N) + +DPI kann pro Doc kalibriert werden (Default 96), gespeichert in doc.Strings. +""" +import os +import sys +import math +import json +import Rhino +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import panel_base + +PANEL_GUID_STR = "5c8e4f3f-6d0e-4f1a-a3d4-e5f607182941" + +# DPI ist eine Hardware-Eigenschaft (Bildschirm) — NICHT pro Dokument speichern. +# Wir legen sie in einer Config-Datei im Home des Users ab. +_DOC_DPI_KEY = "dossier_dpi" # Legacy, fuer Migration + +# Pro Viewport-Name der zuletzt vom User explizit gesetzte Massstab +# (Dropdown/Input/100%-Button/Ausschnitt-Restore). NICHT der Live-Zoom — der +# drifted bei Pan/Zoom. Wird von Ausschnitten beim Speichern als "der +# eingestellte Massstab" gelesen. Per-doc persistiert in doc.Strings als +# JSON-Dict, damit ein Wechsel zurueck auf einen frueher gesetzten Viewport +# den korrekten Wert wieder rausgibt — auch nach Restart. +_user_set_scales = {} # {viewport_name: float} +_user_set_scales_loaded = False # lazy load aus doc.Strings beim ersten Zugriff +_DOC_USER_SCALES_KEY = "dossier_user_scales" # JSON-Dict {vp_name: ratio} +_DOC_USER_SCALE_KEY = "dossier_user_scale" # Legacy: globaler letzter Wert, + # wird fuer Plotweight/Hatch-Rescale + # weiter doc-weit genutzt. +_CONFIG_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel") +_CONFIG_PATH = os.path.join(_CONFIG_DIR, "config.json") +_DEFAULT_DPI = 96.0 + +# Mac WKWebView verschluckt schnelle ExecuteScript-Bursts -> wir limitieren +# das Live-Update auf alle N Idle-Ticks (~5-10/s). +_IDLE_THROTTLE = 6 + + +def _mm_per_doc_unit(doc): + """Faktor: doc-Einheit -> Millimeter.""" + us = doc.ModelUnitSystem + UnitSystem = Rhino.UnitSystem + if us == UnitSystem.Millimeters: return 1.0 + if us == UnitSystem.Centimeters: return 10.0 + if us == UnitSystem.Meters: return 1000.0 + if us == UnitSystem.Kilometers: return 1000000.0 + if us == UnitSystem.Inches: return 25.4 + if us == UnitSystem.Feet: return 304.8 + if us == UnitSystem.Yards: return 914.4 + if us == UnitSystem.Miles: return 1609344.0 + # Fallback ueber Rhinos UnitScale + try: + return Rhino.RhinoMath.UnitScale(us, UnitSystem.Millimeters) + except Exception: + return 1.0 + + +_DETECT_JXA = ( + 'ObjC.import("CoreGraphics");' + 'var id=$.CGMainDisplayID();' + 'var sz=$.CGDisplayScreenSize(id);' + 'var m=$.CGDisplayCopyDisplayMode(id);' + 'JSON.stringify({mm:sz.width,mh:sz.height,' + 'px:$.CGDisplayModeGetPixelWidth(m),' # echte/physische Pixel + 'py:$.CGDisplayModeGetPixelHeight(m),' + 'lpx:$.CGDisplayPixelsWide(id),' # logische Pixel (zum Vergleich) + 'lpy:$.CGDisplayPixelsHigh(id)});' +) + + + +def _detect_dpi(): + """Automatische DPI-Erkennung auf macOS via CoreGraphics. + + Ruft `osascript -l JavaScript` mit einem CoreGraphics-Snippet auf: + CGDisplayScreenSize(mainDisplay) -> physische Groesse in mm + CGDisplayPixelsWide -> logische Aufloesung in px + + DPI = px * 25.4 / mm. Funktioniert auf Intel- und Apple-Silicon-Macs + und passt sich automatisch an macOS-Scaling-Aenderungen an. + + Implementiert ueber System.Diagnostics.Process (IronPython auf .NET + unterstuetzt kein `subprocess.check_output`). + """ + try: + from System.Diagnostics import Process, ProcessStartInfo + except Exception as ex: + print("[MASSSTAB] auto-detect: .NET Process nicht verfuegbar:", ex) + return None + if not os.path.isfile("/usr/bin/osascript"): + # Vermutlich nicht macOS -> nichts zu detecten + return None + # JXA-Snippet in eine temp-Datei schreiben, damit wir uns das + # Shell-Quoting fuer Argumente sparen koennen. + script_path = None + try: + import tempfile + fd, script_path = tempfile.mkstemp(suffix=".js", prefix="rhinopanel_dpi_") + try: + os.write(fd, _DETECT_JXA.encode("utf-8")) + finally: + os.close(fd) + psi = ProcessStartInfo() + psi.FileName = "/usr/bin/osascript" + psi.Arguments = '-l JavaScript "' + script_path + '"' + psi.RedirectStandardOutput = True + psi.RedirectStandardError = True + psi.UseShellExecute = False + psi.CreateNoWindow = True + p = Process.Start(psi) + out = p.StandardOutput.ReadToEnd() + err = p.StandardError.ReadToEnd() + # Timeout-Schutz: maximal 2s warten + try: + finished = p.WaitForExit(2000) + except Exception: + finished = True + if not finished: + try: p.Kill() + except Exception: pass + print("[MASSSTAB] auto-detect: osascript timeout") + return None + if p.ExitCode != 0: + print("[MASSSTAB] auto-detect osascript ExitCode={}:".format(p.ExitCode), err) + return None + import json as _json + data = _json.loads((out or "").strip()) + mm = float(data.get("mm") or 0) + # Rhinos vp.Size auf Mac liefert PHYSISCHE Pixel (Retina-Backing), + # daher physische Pixelzahl fuer die DPI verwenden — sonst sind + # die Modelle nach Skala-Anwendung halb so gross. + px = float(data.get("px") or 0) + lpx = float(data.get("lpx") or 0) + if mm <= 0 or px <= 0: + return None + dpi = px * 25.4 / mm + if dpi < 30.0 or dpi > 600.0: + print("[MASSSTAB] auto-detect: DPI {:.1f} ausserhalb 30..600 -> ignoriert".format(dpi)) + return None + print("[MASSSTAB] DPI auto-detected: {:.1f} physisch (Bildschirm {:.0f}x{:.0f}px / {:.1f}x{:.1f}mm, logisch {:.0f}x{:.0f})".format( + dpi, px, float(data.get("py") or 0), + mm, float(data.get("mh") or 0), + lpx, float(data.get("lpy") or 0))) + return dpi + except Exception as ex: + print("[MASSSTAB] auto-detect fehlgeschlagen:", ex) + return None + finally: + if script_path: + try: os.remove(script_path) + except Exception: pass + + +_config_cache = None # in-memory Cache fuer die Config-Datei (DPI etc.) + + +def _read_config(): + """Liest die Config-Datei nur einmal, danach aus Memory-Cache. + Wird via _compute_scale/_get_dpi sehr haeufig (jeden Idle-Tick) aufgerufen + -> File-IO darf nicht jeden Tick passieren.""" + global _config_cache + if _config_cache is not None: + return _config_cache + cfg = {} + try: + if os.path.isfile(_CONFIG_PATH): + with open(_CONFIG_PATH, "rb") as f: + data = json.loads(f.read().decode("utf-8")) + if isinstance(data, dict): + cfg = data + except Exception as ex: + print("[MASSSTAB] config lesen:", ex) + _config_cache = cfg + return cfg + + +def _write_config(cfg): + """Schreibt + invalidiert den Cache.""" + global _config_cache + try: + if not os.path.isdir(_CONFIG_DIR): + os.makedirs(_CONFIG_DIR) + with open(_CONFIG_PATH, "wb") as f: + f.write(json.dumps(cfg, ensure_ascii=False, indent=2).encode("utf-8")) + _config_cache = cfg # Cache mit dem geschriebenen Stand aktualisieren + return True + except Exception as ex: + print("[MASSSTAB] config schreiben:", ex) + return False + + +def _get_dpi(doc): + """Liefert den aktuell gueltigen DPI-Wert aus der Config. Loest KEINEN + Subprocess aus — Auto-Detection passiert nur explizit (bei READY, + Button-Klick oder via _force_redetect_dpi).""" + cfg = _read_config() + # Legacy-Migration: alter doc.Strings Wert -> einmalig in Config + if doc is not None and "dpi" not in cfg: + try: + legacy = doc.Strings.GetValue(_DOC_DPI_KEY) + if legacy: + v = float(legacy) + if 30.0 <= v <= 600.0: + cfg["dpi"] = v + cfg["dpi_source"] = "manual" + _write_config(cfg) + return v + except Exception: + pass + try: + v = float(cfg.get("dpi")) + if 30.0 <= v <= 600.0: + return v + except Exception: + pass + return _DEFAULT_DPI + + +def _bootstrap_dpi(): + """Beim Panel-Start einmal aufgerufen. Verhalten: + - Keine Config: detected + speichert + - Source 'manual': unangetastet (User hat bewusst gewaehlt) + - Source 'auto'/sonst: detected, ueberschreibt + (so faengt eine neue Detection-Logik auch alte Werte ein und der + User reagiert auf Display-Scaling-Wechsel automatisch) + Subprocess laeuft hier nur einmal pro Panel-Open.""" + cfg = _read_config() + if cfg.get("dpi_source") == "manual": + return # User-Override respektieren + auto = _detect_dpi() + if auto is None: + return + cfg["dpi"] = auto + cfg["dpi_source"] = "auto" + _write_config(cfg) + + +def _set_dpi(doc, value, source="manual"): + try: + v = float(value) + except Exception: + return False + if not (30.0 <= v <= 600.0): + return False + cfg = _read_config() + cfg["dpi"] = v + cfg["dpi_source"] = source + if not _write_config(cfg): + return False + print("[MASSSTAB] DPI={:.1f} ({}) -> {}".format(v, source, _CONFIG_PATH)) + return True + + +def _force_redetect_dpi(): + """Manueller "Auto-Detect"-Trigger via Button. Ueberschreibt auch + einen manuellen Override. Liefert den neuen Wert oder None.""" + auto = _detect_dpi() + if auto is None: + return None + cfg = _read_config() + cfg["dpi"] = auto + cfg["dpi_source"] = "auto" + _write_config(cfg) + return auto + + +def _active_vp(): + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return None, None + view = doc.Views.ActiveView + if view is None: return doc, None + try: + # Page-Views (Layouts) haben einen eigenen Viewport — fuer den + # Massstab interessiert uns dort das aktive Detail. + if isinstance(view, Rhino.Display.RhinoPageView): + for d in view.GetDetailViews(): + if d.IsActive: + return doc, d.Viewport + return doc, view.ActiveViewport + except Exception: + pass + return doc, view.ActiveViewport + + +def _get_frustum_width(vp): + """Liefert die sichtbare Welt-Breite (right - left) oder None.""" + try: + ok, l, r, b, t, n, f = vp.GetFrustum() + if not ok: return None, None + return (r - l), (t - b) + except Exception: + return None, None + + +def _viewport_pixels(vp): + try: + sz = vp.Size + return float(sz.Width), float(sz.Height) + except Exception: + return None, None + + +def _compute_scale(doc, vp): + """Liefert dict mit Scale-Info fuer aktuellen Viewport. + Felder: viewName, parallel, scale (1:N float), pixelWidth, pixelHeight, + unitSystem (string), dpi. + scale=None bei Perspective oder unbekannten Groessen. + """ + cfg = _read_config() + info = { + "viewName": None, + "parallel": False, + "scale": None, + "appliedScale": None, + "pixelWidth": None, + "pixelHeight": None, + "unitSystem": "?", + "dpi": _get_dpi(doc) if doc else _DEFAULT_DPI, + "dpiSource": cfg.get("dpi_source", "default"), + "showLineweights": _get_lineweights_enabled(doc), + } + if vp is None or doc is None: + return info + try: + info["viewName"] = vp.Name + info["parallel"] = bool(vp.IsParallelProjection) + info["unitSystem"] = str(doc.ModelUnitSystem) + except Exception: + pass + # appliedScale pro Viewport. Map ist gefuettert durch _apply_scale und + # Ausschnitt-Restore — wenn ein anderer Viewport aktiv ist als beim letzten + # Setzen, kommt entweder dessen frueher gesetzter Wert oder None zurueck. + # Niemals auf die Live-Skala mappen — das Dropdown soll STATISCH sein. + # Wichtig: nur bei Parallelprojektion zurueckgeben. In Perspective ist ein + # Massstab konzeptionell unsinnig — selbst wenn der gleiche Viewport vorher + # parallel mit Massstab war, soll das Dropdown auf "-" springen, sobald die + # Projektion auf perspective wechselt. + try: + if info.get("parallel"): + v = _get_applied_scale_for_vp(doc, info["viewName"]) + if v is not None and v > 0: + info["appliedScale"] = v + except Exception: + pass + if not info["parallel"]: + return info + fw, fh = _get_frustum_width(vp) + pw, ph = _viewport_pixels(vp) + info["pixelWidth"] = pw + info["pixelHeight"] = ph + if fw is None or pw is None or pw <= 0: + return info + mm_per_u = _mm_per_doc_unit(doc) + dpi = info["dpi"] + if dpi <= 0: + return info + frustum_mm = fw * mm_per_u + screen_mm = pw * 25.4 / dpi + if screen_mm <= 0: + return info + info["scale"] = frustum_mm / screen_mm + return info + + +_LW_KEY = "dossier_show_lineweights" +_LW_ORIG_KEY = "dossier_plotweight_orig" # per-objekt/layer Original-Wert +_HATCH_ORIG_KEY = "dossier_hatch_scale_orig" # auf Hatch-Attrs: original PatternScale + + +def _apply_scaled_lineweights(doc, enabled, scale_n): + """Skaliert die PlotWeights aller Layer + Objekte mit scale_n wenn + enabled, sonst stellt Originalwerte wieder her. Originals werden in + UserStrings persistiert -> idempotent, ueberlebt Restart. + + Default-Werte (0 oder -1 = "by parent") werden nicht angefasst — sonst + wuerden eigentlich-default-Objekte plotweight-fixiert. + """ + if doc is None: return + if scale_n is None or scale_n <= 0: scale_n = 1.0 + factor = float(scale_n) if enabled else 1.0 + n_layer = 0 + n_obj = 0 + + # -- Layer --------------------------------------------------------------- + try: + for layer in doc.Layers: + if layer is None or layer.IsDeleted: + continue + try: + stored = layer.GetUserString(_LW_ORIG_KEY) + if stored: + orig = float(stored) + else: + orig = float(layer.PlotWeight) if layer.PlotWeight else 0.0 + # Nur sichern wenn ueberhaupt was anzuspielen ist + if orig > 0: + layer.SetUserString(_LW_ORIG_KEY, "{:.6f}".format(orig)) + if orig <= 0: + continue # default-Layer (hairline) — nicht anfassen + new = orig * factor + if abs(float(layer.PlotWeight) - new) > 1e-6: + layer.PlotWeight = new + n_layer += 1 + except Exception as ex: + print("[MASSSTAB] LW scale layer '{}': {}".format(layer.Name, ex)) + except Exception as ex: + print("[MASSSTAB] LW scale layers:", ex) + + # -- Objekte ------------------------------------------------------------- + try: + for obj in doc.Objects: + if obj is None or obj.IsDeleted: + continue + try: + a = obj.Attributes + stored = a.GetUserString(_LW_ORIG_KEY) + if stored: + orig = float(stored) + elif a.PlotWeight and a.PlotWeight > 0: + orig = float(a.PlotWeight) + new_a = a.Duplicate() + new_a.SetUserString(_LW_ORIG_KEY, "{:.6f}".format(orig)) + doc.Objects.ModifyAttributes(obj, new_a, True) + else: + continue # PlotWeightSource ist "by layer" — nichts zu tun + new = orig * factor + if abs(float(a.PlotWeight) - new) > 1e-6: + new_a = a.Duplicate() + new_a.PlotWeight = new + doc.Objects.ModifyAttributes(obj, new_a, True) + n_obj += 1 + except Exception: + pass + except Exception as ex: + print("[MASSSTAB] LW scale objects:", ex) + + try: doc.Views.Redraw() + except Exception: pass + print("[MASSSTAB] PlotWeight-Skalierung x{:.1f}: {} Layer, {} Objekte angepasst".format( + factor, n_layer, n_obj)) + # Diagnose: zeige die ersten paar Layer mit ihren echten PlotWeights + try: + shown = 0 + for layer in doc.Layers: + if layer.IsDeleted or not layer.PlotWeight: continue + stored = layer.GetUserString(_LW_ORIG_KEY) or "-" + print("[MASSSTAB] Layer '{}' PlotWeight={:.3f}mm (orig={})".format( + layer.Name, float(layer.PlotWeight), stored)) + shown += 1 + if shown >= 5: break + except Exception: pass + + # (Hatch-Skalierung ist in apply_scaled_hatches ausgelagert — die laeuft + # IMMER, unabhaengig vom Print-Mode, weil Hatches sich am Massstab + # orientieren sollten unabhaengig von Plot-Weight-Anzeige.) + + +def write_plotweight(doc, target, value): + """Setze PlotWeight auf target (Layer oder ObjectAttributes-Duplicate), + Print-Mode-aware. value = "echter" Wert in mm wie er auf Papier landet. + + Speichert value als Original-UserString. Wenn Print-Mode aktiv ist wird + PlotWeight = value * scale gesetzt damit die Anzeige direkt skaliert. + + Aufrufer ist verantwortlich fuer ModifyAttributes / Doc-Refresh.""" + if target is None: return + try: + v = float(value) if value is not None else 0.0 + except Exception: + v = 0.0 + try: + if v > 0: + target.SetUserString(_LW_ORIG_KEY, "{:.6f}".format(v)) + else: + target.SetUserString(_LW_ORIG_KEY, "") + except Exception: + pass + print_on = _get_lineweights_enabled(doc) if doc is not None else False + factor = _read_user_scale(doc, default=1.0) if print_on else 1.0 + try: + target.PlotWeight = v * factor + except Exception as ex: + print("[MASSSTAB] write_plotweight set:", ex) + + +def apply_scaled_hatches(doc, scale_n): + """Skaliert alle Hatches im Doc gemaess Massstab scale_n. + + Formel: factor = sqrt(N) / 10 — moderate Skalierung mit 1:100 als + Referenz-Punkt (factor = 1.0). + 1:1 -> 0.10 + 1:50 -> 0.71 + 1:100 -> 1.0 (Referenz) + 1:500 -> 2.24 + 1:1000 -> 3.16 + + Originale werden in Hatch-Attributes UserString gesichert. + Returns: (n_gefunden, n_skaliert) + """ + import math + if doc is None: return (0, 0) + if not scale_n or scale_n <= 0: scale_n = 1.0 + factor = math.sqrt(float(scale_n)) / 10.0 + # IDs vorab sammeln (sonst invalidiert Replace die Iteration) + hatch_ids = [] + try: + settings = Rhino.DocObjects.ObjectEnumeratorSettings() + settings.ObjectTypeFilter = Rhino.DocObjects.ObjectType.Hatch + settings.NormalObjects = True + settings.LockedObjects = True + settings.HiddenObjects = True + settings.IncludeLights = False + settings.IncludeGrips = False + for obj in doc.Objects.GetObjectList(settings): + if obj is not None and not obj.IsDeleted: + hatch_ids.append(obj.Id) + except Exception: + try: + for obj in doc.Objects: + if obj is None or obj.IsDeleted: continue + try: + if obj.ObjectType == Rhino.DocObjects.ObjectType.Hatch: + hatch_ids.append(obj.Id) + except Exception: pass + except Exception: pass + + n_scaled = 0 + for hid in hatch_ids: + try: + obj = doc.Objects.FindId(hid) + if obj is None or obj.IsDeleted: continue + g = obj.Geometry + a = obj.Attributes + stored = a.GetUserString(_HATCH_ORIG_KEY) + if stored: + try: orig = float(stored) + except Exception: orig = 0.0 + else: + cur = float(g.PatternScale) if g.PatternScale else 0.0 + if cur > 0: + orig = cur + new_a = a.Duplicate() + new_a.SetUserString(_HATCH_ORIG_KEY, "{:.6f}".format(orig)) + doc.Objects.ModifyAttributes(obj, new_a, True) + else: + continue + if orig <= 0: continue + new_val = orig * factor + if abs(float(g.PatternScale) - new_val) > 1e-6: + new_g = g.Duplicate() + try: + new_g.PatternScale = new_val + if doc.Objects.Replace(hid, new_g): + n_scaled += 1 + except Exception as ex: + print("[MASSSTAB] hatch set PatternScale:", ex) + except Exception as ex: + print("[MASSSTAB] hatch iter:", ex) + if n_scaled or hatch_ids: + print("[MASSSTAB] Hatch-Skalierung: {} gefunden, {} mit Faktor x{:.2f} angepasst".format( + len(hatch_ids), n_scaled, factor)) + try: doc.Views.Redraw() + except Exception: pass + return (len(hatch_ids), n_scaled) + + +def post_create_hatch_scale(doc, hatch_obj, user_scale): + """Nach AddHatch oder Replace einer Hatch aufrufen. + Speichert user_scale als Original-UserString und skaliert die Geometrie + mit sqrt(N) (siehe apply_scaled_hatches).""" + import math + if doc is None or hatch_obj is None: return + try: + u = float(user_scale) + except Exception: + return + if u <= 0: return + try: + a = hatch_obj.Attributes.Duplicate() + a.SetUserString(_HATCH_ORIG_KEY, "{:.6f}".format(u)) + doc.Objects.ModifyAttributes(hatch_obj, a, True) + except Exception as ex: + print("[MASSSTAB] post_create_hatch_scale orig:", ex) + # Mit aktuellem Massstab skalieren (sqrt-Formel /10, siehe apply_scaled_hatches) + scale_n = _read_user_scale(doc, default=1.0) + if not scale_n or scale_n <= 0: scale_n = 1.0 + if abs(scale_n - 1.0) < 1e-6: return + factor = math.sqrt(float(scale_n)) / 10.0 + try: + h2 = doc.Objects.FindId(hatch_obj.Id) + if h2 is None: return + new_g = h2.Geometry.Duplicate() + new_g.PatternScale = u * factor + doc.Objects.Replace(h2.Id, new_g) + except Exception as ex: + print("[MASSSTAB] post_create_hatch_scale rescale:", ex) + + +def read_plotweight(target): + """Lese den "echten" PlotWeight (vor Print-Mode-Skalierung). Faellt auf + das aktuelle PlotWeight zurueck wenn kein Original gespeichert ist.""" + if target is None: return 0.0 + try: + s = target.GetUserString(_LW_ORIG_KEY) + if s: + return float(s) + except Exception: + pass + try: + v = float(target.PlotWeight) if target.PlotWeight else 0.0 + return v if v > 0 else 0.0 + except Exception: + return 0.0 + + +def _get_lineweights_enabled(doc): + """Print-View / Strichstaerken-Anzeige. Default OFF — Rhino zeigt sonst + Linien als Hairlines was beim Zeichnen praktischer ist.""" + if doc is None: return False + try: + v = doc.Strings.GetValue(_LW_KEY) + if v is None: return False + return v == "1" + except Exception: + return False + + +def _set_lineweights_enabled(doc, enabled): + """Togglet Rhinos Print-Display (zeigt PlotWeights mit ihren echten + Strichstaerken anstatt Hairlines), persistiert den State im Doc UND + skaliert die PlotWeights mit dem aktuellen Massstab. + + Auf Mac heisst der relevante Befehl _PrintDisplay (nicht _LineWeights — + das oeffnet einen Dialog). Wir feuern beide Varianten als Fallback.""" + if doc is None: return False + flag = "1" if enabled else "0" + try: + doc.Strings.SetString(_LW_KEY, flag) + except Exception as ex: + print("[MASSSTAB] _set_lineweights_enabled persist:", ex) + # Print-Display togglen — primaerer Befehl auf Mac Rhino + on_off = "_On" if enabled else "_Off" + yes_no = "_Yes" if enabled else "_No" + cmds = [ + "_-PrintDisplay _State {} _Enter".format(on_off), + "_-LineweightsDisplay _State {} _Enter".format(on_off), + "_-LineWeights _DisplayLineweights {} _Enter".format(yes_no), + ] + for c in cmds: + try: + Rhino.RhinoApp.RunScript(c, False) + except Exception: + pass + # PlotWeight-Skalierung mit aktuellem Massstab + try: + scale_n = _read_user_scale(doc, default=1.0) + _apply_scaled_lineweights(doc, enabled, scale_n) + except Exception as ex: + print("[MASSSTAB] PlotWeight-Scale:", ex) + try: + for v in doc.Views: v.Redraw() + except Exception: pass + print("[MASSSTAB] Print-Display:", "AN (Strichstaerken sichtbar)" if enabled else "AUS") + return True + + +def _read_user_scale(doc, default=1.0): + """Persistierter eingestellter Massstab oder default. Setze default=None + um "nie gesetzt" zu erkennen.""" + if doc is None: return default + try: + raw = doc.Strings.GetValue(_DOC_USER_SCALE_KEY) + if raw: + v = float(raw) + if v > 0: return v + except Exception: + pass + return default + + +def _write_user_scale(doc, ratio): + if doc is None: return + try: + doc.Strings.SetString(_DOC_USER_SCALE_KEY, "{:.6f}".format(float(ratio))) + except Exception as ex: + print("[MASSSTAB] _write_user_scale:", ex) + + +def _ensure_user_scales_loaded(doc): + """Liest die persistierte per-Viewport-Map einmal pro Session aus doc.Strings.""" + global _user_set_scales_loaded + if _user_set_scales_loaded or doc is None: + return + try: + raw = doc.Strings.GetValue(_DOC_USER_SCALES_KEY) + if raw: + data = json.loads(raw) + if isinstance(data, dict): + for k, v in data.items(): + try: + f = float(v) + if f > 0 and k: + _user_set_scales[str(k)] = f + except Exception: + pass + except Exception as ex: + print("[MASSSTAB] _ensure_user_scales_loaded:", ex) + _user_set_scales_loaded = True + + +def _write_user_scales(doc): + if doc is None: return + try: + doc.Strings.SetString(_DOC_USER_SCALES_KEY, + json.dumps(_user_set_scales, ensure_ascii=False)) + except Exception as ex: + print("[MASSSTAB] _write_user_scales:", ex) + + +def _get_applied_scale_for_vp(doc, vp_name): + """Eingestellter Massstab fuer einen Viewport, oder None.""" + if not vp_name: return None + _ensure_user_scales_loaded(doc) + return _user_set_scales.get(vp_name) + + +def _set_applied_scale_for_vp(doc, vp_name, ratio): + if not vp_name: return + _ensure_user_scales_loaded(doc) + _user_set_scales[vp_name] = float(ratio) + _write_user_scales(doc) + + +def _rescale_doc_patterns(doc, factor): + """Multipliziert alle Hatch-PatternScales und per-objekt Linetype-Scales + um factor. Iteriert das gesamte Doc — laeuft auch bei 1000+ Objekten + in unter 100ms (standard CAD-Verhalten). + + Globale Linetype-Skala wird auch versucht zu setzen (Rhino-API + inkonsistent ueber Versionen — wir probieren mehrere Property-Namen).""" + if factor is None or factor <= 0 or abs(factor - 1.0) < 1e-9: + return # nichts zu tun + n_h = 0 + n_l = 0 + HatchT = Rhino.Geometry.Hatch + try: + for obj in doc.Objects: + if obj is None or obj.IsDeleted: + continue + g = None + try: g = obj.Geometry + except Exception: g = None + # Hatch-Geometrie -> PatternScale skalieren + if g is not None and isinstance(g, HatchT): + try: + g2 = g.Duplicate() + cur = float(g2.PatternScale) if g2.PatternScale else 1.0 + g2.PatternScale = cur * factor + doc.Objects.Replace(obj.Id, g2) + n_h += 1 + except Exception as ex: + print("[MASSSTAB] hatch rescale:", ex) + # Per-Objekt Linetype-Scale (Rhino 8 Attribut) + try: + a = obj.Attributes + for prop in ("LinetypePatternLengthScale", "LinetypeScale"): + if hasattr(a, prop): + cur = getattr(a, prop) + if cur and cur > 0 and abs(cur - 1.0) > 1e-9: + # Nur Objekte mit explizit gesetzter Skala anfassen + # (Default=1.0 ueberlassen wir dem globalen Multiplikator). + new_a = a.Duplicate() + setattr(new_a, prop, cur * factor) + doc.Objects.ModifyAttributes(obj, new_a, True) + n_l += 1 + break + except Exception: + pass + except Exception as ex: + print("[MASSSTAB] _rescale_doc_patterns:", ex) + + # Globale Linetype-Pattern-Length-Skala (Rhino-doc-Setting) versuchen. + # Property-Namen variieren je nach Version — wir probieren. + set_global = False + for prop in ("LinetypeAndPatternScale", + "LinetypeAndPatternLengthScale", + "ModelSpaceLinetypeScale"): + try: + if hasattr(doc, prop): + cur = float(getattr(doc, prop)) + if cur > 0: + setattr(doc, prop, cur * factor) + set_global = True + break + except Exception: + pass + try: doc.Views.Redraw() + except Exception: pass + print("[MASSSTAB] Rescale x{:.4f}: {} Hatches, {} per-obj Linetypes{}".format( + factor, n_h, n_l, ", global Linetype-Scale" if set_global else "")) + + +def get_current_massstab_factor(doc=None): + """Multiplier fuer Pattern-Skalen — aktuell deaktiviert (Faktor 1.0). + Massstab beeinflusst aktuell NUR den Viewport-Zoom, nicht die Pattern- + Skalen oder Strichstaerken.""" + return 1.0 + + +def _apply_scale(doc, vp, ratio): + """Setzt den Frustum so dass 1mm Bildschirm == ratio doc-Einheiten in mm. + ratio = 1:N -> ratio_value = N. Liefert True bei Erfolg. + """ + if vp is None or doc is None: return False + try: + if not vp.IsParallelProjection: + print("[MASSSTAB] Viewport ist nicht parallel — Skala nicht setzbar") + return False + except Exception: + return False + pw, ph = _viewport_pixels(vp) + if pw is None or pw <= 0: + return False + dpi = _get_dpi(doc) + mm_per_u = _mm_per_doc_unit(doc) + if mm_per_u <= 0 or dpi <= 0: + return False + screen_mm = pw * 25.4 / dpi + new_frustum_mm = screen_mm * float(ratio) + new_frustum_u = new_frustum_mm / mm_per_u # in doc-units + try: + ok, l, r, b, t, n, f = vp.GetFrustum() + if not ok: return False + cur_w = (r - l) + if cur_w <= 0: return False + # RhinoViewport.SetFrustum existiert nicht — wir benutzen Magnify(factor). + # factor > 1 zoomt rein (kleineres Frustum). factor = cur_w / new_w. + factor = cur_w / new_frustum_u + if factor <= 0 or not (factor < 1e9 and factor > 1e-9): + print("[MASSSTAB] _apply_scale: ungueltiger Faktor", factor) + return False + applied = False + # Verschiedene API-Signaturen je nach Rhino-Version durchprobieren. + # 1) Magnify(factor, mode) — mode=False -> mittig + try: + vp.Magnify(float(factor), False) + applied = True + except Exception as ex1: + # 2) Magnify(factor) + try: + vp.Magnify(float(factor)) + applied = True + except Exception as ex2: + # 3) Camera-Skript-Fallback (zoom faktorisch via _Zoom Factor) + try: + Rhino.RhinoApp.RunScript("_-Zoom _Factor {:.6f} _Enter".format(factor), False) + applied = True + except Exception as ex3: + print("[MASSSTAB] _apply_scale alle Varianten fehlgeschlagen:", + ex1, ex2, ex3) + if not applied: + return False + # PlotWeights nur skalieren wenn Print-Mode aktiv ist. + try: + if _get_lineweights_enabled(doc): + _apply_scaled_lineweights(doc, True, float(ratio)) + except Exception as ex: + print("[MASSSTAB] LW-Rescale:", ex) + # Hatches mit sqrt(N) skalieren — moderate Anpassung. + try: + apply_scaled_hatches(doc, float(ratio)) + except Exception as ex: + print("[MASSSTAB] Hatch-Rescale:", ex) + # Neuen Wert persistieren — sowohl per-Viewport (fuer das Dropdown, + # damit jeder Viewport seinen eigenen Massstab behaelt) als auch als + # globaler "letzter Wert" (Legacy-Key; wird von Plotweight/Hatch-Rescale + # doc-weit benutzt — dort ist nur EIN Faktor sinnvoll). + _write_user_scale(doc, ratio) + try: + _set_applied_scale_for_vp(doc, vp.Name, float(ratio)) + except Exception as ex: + print("[MASSSTAB] per-vp scale write:", ex) + try: doc.Views.Redraw() + except Exception: pass + print("[MASSSTAB] Skala 1:{:.2f} gesetzt (Faktor {:.4f}, soll-frustum {:.4f} {})".format( + ratio, factor, new_frustum_u, str(doc.ModelUnitSystem))) + return True + except Exception as ex: + print("[MASSSTAB] _apply_scale:", ex) + return False + + +def _zoom_extents(doc, vp, selected_only=False): + if vp is None or doc is None: return False + try: + if selected_only: + objs = list(doc.Objects.GetSelectedObjects(False, False)) + if not objs: + print("[MASSSTAB] Keine Selektion fuer Zoom-Selection") + return False + bbox = Rhino.Geometry.BoundingBox.Empty + for o in objs: + try: + b = o.Geometry.GetBoundingBox(True) + if bbox.IsValid: + bbox.Union(b) + else: + bbox = b + except Exception: + continue + if not bbox.IsValid: + return False + # Etwas Padding (10%) + d = bbox.Diagonal + pad = max(d.X, d.Y, d.Z) * 0.05 + if pad > 0: + bbox.Inflate(pad, pad, pad) + try: + vp.ZoomBoundingBox(bbox) + except Exception: + # Fallback ueber Rhino-Skript + Rhino.RhinoApp.RunScript("_-Zoom _Selected _Enter", False) + else: + try: + vp.ZoomExtents() + except Exception: + Rhino.RhinoApp.RunScript("_-Zoom _All _Extents _Enter", False) + try: doc.Views.Redraw() + except Exception: pass + return True + except Exception as ex: + print("[MASSSTAB] _zoom_extents:", ex) + return False + + +# --- Bridge ----------------------------------------------------------------- + +class MassstabBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "massstab") + self._idle_counter = 0 + self._last_info = None + + def _on_ready(self): + # Einmalige Bootstrap-Detection falls noch keine DPI in der Config. + try: _bootstrap_dpi() + except Exception as ex: print("[MASSSTAB] bootstrap:", ex) + self._send_state(force=True) + + def handle(self, data): + if not isinstance(data, dict): + return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): p = {} + + if t == "READY": + self._on_ready() + elif t == "REQUEST_STATE": + self._send_state(force=True) + elif t == "SET_SCALE": + doc, vp = _active_vp() + try: + ratio = float(p.get("ratio")) + except Exception: + return + if ratio <= 0: return + if _apply_scale(doc, vp, ratio): + self._send_state(force=True) + elif t == "ZOOM_ONE_TO_ONE": + doc, vp = _active_vp() + if _apply_scale(doc, vp, 1.0): + self._send_state(force=True) + elif t == "ZOOM_EXTENTS": + doc, vp = _active_vp() + _zoom_extents(doc, vp, selected_only=False) + self._send_state(force=True) + elif t == "ZOOM_SELECTION": + doc, vp = _active_vp() + _zoom_extents(doc, vp, selected_only=True) + self._send_state(force=True) + elif t == "SET_DPI": + doc, _ = _active_vp() + if _set_dpi(doc, p.get("dpi"), source="manual"): + self._send_state(force=True) + elif t == "DETECT_DPI": + v = _force_redetect_dpi() + if v is None: + print("[MASSSTAB] Auto-Detect: keine Bildschirminfo verfuegbar") + self._send_state(force=True) + elif t == "SET_LINEWEIGHTS": + doc, _ = _active_vp() + _set_lineweights_enabled(doc, bool(p.get("enabled"))) + self._send_state(force=True) + + def _send_state(self, force=False): + doc, vp = _active_vp() + info = _compute_scale(doc, vp) + # Vergleich gegen letzten Stand — Nachrichten sparen + if not force and info == self._last_info: + return + self._last_info = info + self.send("STATE", info) + + def tick_idle(self): + self._idle_counter += 1 + if self._idle_counter < _IDLE_THROTTLE: + return + self._idle_counter = 0 + self._send_state(force=False) + + +# --- Listener fuer Live-Updates --------------------------------------------- + +def _install_listeners(bridge): + flag = "massstab_listeners" + sc.sticky["massstab_bridge"] = bridge + if sc.sticky.get(flag): + return + + def on_idle(s, e): + b = sc.sticky.get("massstab_bridge") + if b is not None: + try: b.tick_idle() + except Exception: pass + + def on_view_change(*args): + b = sc.sticky.get("massstab_bridge") + if b is not None: + try: b._send_state(force=True) + except Exception: pass + + Rhino.RhinoApp.Idle += on_idle + Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change + sc.sticky[flag] = True + print("[MASSSTAB] Listener aktiv (Idle-Poll + Doc-Change)") + + +def get_current_scale_ratio(): + """Aktuelle LIVE-Skala 1:N als Float (aus Viewport-Frustum berechnet). + Drifted bei Pan/Zoom mit. None bei Perspective.""" + doc, vp = _active_vp() + info = _compute_scale(doc, vp) + return info.get("scale") + + +def get_applied_scale_ratio(): + """Eingestellter Massstab fuer den aktuell aktiven Viewport, oder None. + Wird vom Ausschnitt-Capture als "der gerade gepinnte Massstab" gelesen.""" + doc, vp = _active_vp() + if doc is None or vp is None: + return None + try: + v = _get_applied_scale_for_vp(doc, vp.Name) + if v is not None and v > 0: + return v + except Exception: + pass + return None + + +def _bridge_factory(): + b = MassstabBridge() + _install_listeners(b) + return b + + +# Hinweis: das eigenstaendige MASSSTAB-Panel wird nicht mehr automatisch +# registriert — OBERLEISTE enthaelt seit der Refactor alle Funktionen. +# Das Modul bleibt als Library bestehen (massstab._compute_scale, +# get_applied_scale_ratio, write_plotweight, read_plotweight, ...). +# Falls du das Panel doch wieder als separaten Tab willst, einfach +# register_standalone_panel() aufrufen oder die Zeile darunter auskommentieren. + +def register_standalone_panel(): + panel_base.register_and_open("massstab", "MASSSTAB", PANEL_GUID_STR, _bridge_factory, + icon_spec=("M", "#c87050")) + +# register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py new file mode 100644 index 0000000..c411e2b --- /dev/null +++ b/rhino/oberleiste.py @@ -0,0 +1,513 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +oberleiste.py +OBERLEISTE-Panel: horizontale Top-Bar mit Architektur-Kontext-Controls. +Vereint View-Switcher, Display-Mode, Massstab, Print-View und Snap-Toggles. + +Re-used massstab-Modul fuer Skala/PlotWeight-Logik — die Bridge proxiet alle +Massstab-bezogenen Nachrichten dorthin. +""" +import os +import sys +import Rhino +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import panel_base +import massstab +import overrides + +PANEL_GUID_STR = "7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" +OVERRIDES_PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62" + + +def _run(cmd): + """Hilfsfunktion: Rhino-Befehl ausfuehren, mit Logging.""" + try: + Rhino.RhinoApp.RunScript(cmd, False) + except Exception as ex: + print("[OBERLEISTE] RunScript-Fehler ({}): {}".format(cmd, ex)) + + +def _get_active_viewport_name(): + try: + v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView + return v.ActiveViewport.Name if v else None + except Exception: + return None + + +def _list_all_command_names(): + """Enumeriert alle registrierten Rhino-Commands (englische Namen). + Wird einmalig beim Bridge-Start aufgerufen und gecached.""" + names = set() + # Variante 1: statische API Rhino.Commands.Command.GetCommandNames + try: + all_names = Rhino.Commands.Command.GetCommandNames(True, True) + for n in all_names: + if n and isinstance(n, str): + names.add(n) + except Exception: + pass + # Variante 2: ueber alle PlugIns iterieren (Fallback) + if not names: + try: + for guid in Rhino.Plugins.PlugIn.GetInstalledPlugIns().Keys: + try: + pi = Rhino.Plugins.PlugIn.Find(guid) + if pi is None: continue + cmds = pi.GetCommands() if hasattr(pi, "GetCommands") else [] + for cmd_guid in cmds: + try: + n = Rhino.Commands.Command.GetCommandName(cmd_guid) + if n: names.add(n) + except Exception: pass + except Exception: pass + except Exception: pass + # Variante 3: minimaler Fallback fuer den Fall dass keine API greift + if not names: + for n in ("Line","Polyline","Rectangle","Circle","Arc","Curve","Text","Hatch", + "Move","Copy","Rotate","Scale","Mirror","Offset","Trim","Extend", + "Join","Explode","Fillet","Array","Box","ExtrudeCrv","BooleanUnion", + "BooleanDifference","BooleanIntersection","Cap","Section","Loft", + "Zoom","Pan","Top","Front","Right","Perspective","Undo","Redo", + "Group","Ungroup","Hide","Show","Delete","SelAll","SelNone", + "Properties","Layer","Snap","Ortho","Planar","Save","SaveAs"): + names.add(n) + out = sorted(names) + print("[OBERLEISTE] {} Rhino-Commands fuer Autocomplete enumeriert".format(len(out))) + return out + + +def _get_command_prompt(): + """Liefert den aktuellen Rhino-Command-Prompt oder leeren String. + Wird gepollt damit OBERLEISTE den Prompt + Optionen anzeigen kann.""" + try: + p = Rhino.RhinoApp.CommandPrompt + return p if p is not None else "" + except Exception: + return "" + + +def _parse_command_options(prompt): + """Extrahiert Option-Tokens aus einem Rhino-Prompt. + Beispiele: + "Line: First point ( BothSides Bisector Length Vertical Angle )" + "Polyline: Next point of polyline ( Close Helix Mode=Persistent Undo )" + Liefert Liste von dicts: [{name, value (optional), token}]. + """ + import re + if not prompt: return [] + # Inhalt der letzten Klammer + m = re.search(r"\(([^()]+)\)\s*$", prompt) + if not m: return [] + body = m.group(1).strip() + options = [] + for tok in body.split(): + tok = tok.strip().rstrip(",;:") + if not tok: continue + if "=" in tok: + name, val = tok.split("=", 1) + options.append({"name": name, "value": val, "token": tok}) + else: + options.append({"name": tok, "value": None, "token": tok}) + return options + + +def _get_active_display_mode_name(): + try: + v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView + if v is None: return None + dm = v.ActiveViewport.DisplayMode + return dm.LocalName if dm else None + except Exception: + return None + + +_display_modes_cache = None # gecacht — Liste aendert sich pro Rhino-Session selten + + +def _list_display_modes(): + """Alle verfuegbaren Display-Modes (LocalName + Id-String). + Gecacht — Liste aendert sich nur wenn User Display-Modes ergaenzt/loescht. + Bei Bedarf kann _display_modes_cache von aussen auf None gesetzt werden.""" + global _display_modes_cache + if _display_modes_cache is not None: + return _display_modes_cache + out = [] + try: + for dm in Rhino.Display.DisplayModeDescription.GetDisplayModes(): + try: + out.append({"name": dm.LocalName, "id": str(dm.Id)}) + except Exception: + continue + except Exception as ex: + print("[OBERLEISTE] _list_display_modes:", ex) + _display_modes_cache = out + return out + + +def _set_display_mode(name): + """Setzt Display-Mode des aktiven Viewports per Name.""" + try: + v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView + if v is None: return False + for dm in Rhino.Display.DisplayModeDescription.GetDisplayModes(): + if dm.LocalName == name or dm.EnglishName == name: + v.ActiveViewport.DisplayMode = dm + v.Redraw() + print("[OBERLEISTE] Display-Mode: {}".format(name)) + return True + except Exception as ex: + print("[OBERLEISTE] _set_display_mode:", ex) + return False + + +# --- Snap / Ortho via ModelAidSettings -------------------------------------- + +def _get_snap_state(): + try: + s = Rhino.ApplicationSettings.ModelAidSettings + return { + "ortho": bool(s.Ortho), + "gridSnap": bool(s.GridSnap), + "osnap": bool(s.UseHorizontalDialog) if False else bool(getattr(s, "Osnap", False)) or False, + "planar": bool(getattr(s, "ProjectOsnapsToCPlane", False)), + } + except Exception: + return {"ortho": False, "gridSnap": False, "osnap": False, "planar": False} + + +def _set_ortho(v): + try: + Rhino.ApplicationSettings.ModelAidSettings.Ortho = bool(v) + except Exception as ex: + print("[OBERLEISTE] _set_ortho:", ex) + + +def _set_grid_snap(v): + try: + Rhino.ApplicationSettings.ModelAidSettings.GridSnap = bool(v) + except Exception as ex: + print("[OBERLEISTE] _set_grid_snap:", ex) + + +def _set_osnap_master(v): + """Master-Toggle fuer Object-Snap (alle aktiven Snaps).""" + try: + s = Rhino.ApplicationSettings.ModelAidSettings + if hasattr(s, "Osnap"): + s.Osnap = bool(v) + elif hasattr(s, "UsePoints"): + # Fallback: einzelne Modi durch + pass + except Exception as ex: + print("[OBERLEISTE] _set_osnap_master:", ex) + + +# --- Bridge ----------------------------------------------------------------- + +class OberleisteBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "oberleiste") + self._idle_counter = 0 + self._last_prompt = "" + self._last_state_sig = None # Fingerprint des letzten Push — dedupe + self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update + # Command-Liste einmalig laden (kann teuer sein -> cachen) + try: + self._all_commands = _list_all_command_names() + except Exception as ex: + print("[OBERLEISTE] command-enum:", ex) + self._all_commands = [] + + def _on_ready(self): + # Bootstrap DPI (gemeinsam mit massstab.py) + try: massstab._bootstrap_dpi() + except Exception: pass + # WebView wurde (neu) gemountet — Frontend-State ist leer, also one-shot + # Listen (displayModes, allCommands) neu mitsenden. Sonst zeigt das + # Display-Dropdown nach einem Re-Mount (z.B. Andocken, Layout-Wechsel) + # nur die "—"-Option und wirkt wie ein toter Button. + self._dm_sent = False + self._commands_sent = False + self._send_state(force=True) + + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): p = {} + + # --- Lifecycle -------------------------------------------------- + if t == "READY": + self._on_ready() + elif t == "REQUEST_STATE": + self._send_state(force=True) + + # --- Massstab (delegiert an massstab-Modul) --------------------- + elif t == "SET_SCALE": + doc, vp = massstab._active_vp() + try: ratio = float(p.get("ratio")) + except Exception: return + if ratio > 0 and massstab._apply_scale(doc, vp, ratio): + self._send_state(force=True) + elif t == "ZOOM_EXTENTS": + doc, vp = massstab._active_vp() + massstab._zoom_extents(doc, vp, selected_only=False) + self._send_state(force=True) + elif t == "ZOOM_SELECTION": + doc, vp = massstab._active_vp() + massstab._zoom_extents(doc, vp, selected_only=True) + self._send_state(force=True) + elif t == "SET_LINEWEIGHTS": + doc, _ = massstab._active_vp() + massstab._set_lineweights_enabled(doc, bool(p.get("enabled"))) + self._send_state(force=True) + elif t == "SET_DPI": + doc, _ = massstab._active_vp() + massstab._set_dpi(doc, p.get("dpi"), source="manual") + self._send_state(force=True) + elif t == "DETECT_DPI": + massstab._force_redetect_dpi() + self._send_state(force=True) + + # --- View-Switcher ---------------------------------------------- + elif t == "SET_VIEW": + v = p.get("view") + if v in ("Top", "Front", "Right", "Perspective", "Left", "Back", "Bottom"): + _run("_-{} _Enter".format(v)) + self._send_state(force=True) + + # --- Display-Mode ----------------------------------------------- + elif t == "SET_DISPLAY_MODE": + n = p.get("name") + if n: + _set_display_mode(n) + self._send_state(force=True) + + # --- Snap-Toggles ----------------------------------------------- + elif t == "TOGGLE_ORTHO": + _set_ortho(bool(p.get("enabled"))) + self._send_state(force=True) + elif t == "TOGGLE_GRID_SNAP": + _set_grid_snap(bool(p.get("enabled"))) + self._send_state(force=True) + elif t == "TOGGLE_OSNAP": + _set_osnap_master(bool(p.get("enabled"))) + self._send_state(force=True) + + # --- Graphical Overrides ---------------------------------------- + elif t == "TOGGLE_OVERRIDES": + doc = Rhino.RhinoDoc.ActiveDoc + overrides.set_enabled(doc, bool(p.get("enabled"))) + self._cached_overrides = None # Cache invalidieren + self._send_state(force=True) + elif t == "SET_OVERRIDES_PRESET": + doc = Rhino.RhinoDoc.ActiveDoc + name = p.get("name") or None + overrides.set_active_preset(doc, name) + self._cached_overrides = None # Cache invalidieren + self._send_state(force=True) + # OVERRIDES-Panel mit-informieren: dort haben sich die Rules + # geaendert (Preset wurde reingeladen). + try: + b = sc.sticky.get("overrides_bridge") + if b is not None: + b._send_state() + except Exception as ex: + print("[OBERLEISTE] notify overrides:", ex) + elif t == "SAVE_OVERRIDES_PRESET": + # Quick-Save direkt aus der Topbar: aktuelle Doc-Rules unter + # gegebenem Namen ablegen und sofort als activePreset markieren. + # Spart dem User den Umweg ueber den grossen OVERRIDES-Editor. + doc = Rhino.RhinoDoc.ActiveDoc + name = (p.get("name") or "").strip() + if not name: + pass + else: + cfg = overrides.load_config(doc) + rules = cfg.get("rules") or [] + if overrides.save_preset(name, rules): + overrides.set_active_preset(doc, name) + self._cached_overrides = None + self._send_state(force=True) + try: + b = sc.sticky.get("overrides_bridge") + if b is not None: + b._send_state() + except Exception as ex: + print("[OBERLEISTE] notify overrides:", ex) + elif t == "OPEN_OVERRIDES_PANEL": + try: + import System + import Rhino.UI as RhinoUI + RhinoUI.Panels.OpenPanel(System.Guid(OVERRIDES_PANEL_GUID_STR)) + except Exception as ex: + print("[OBERLEISTE] OpenPanel Overrides:", ex) + + # --- Command-Line Integration ----------------------------------- + elif t == "RUN_COMMAND": + cmd = (p.get("cmd") or "").strip() + if cmd: + # Auto-Praefix mit "_" falls nicht vorhanden, damit auch + # lokalisierte Rhino-Installationen die EN-Namen verstehen. + if not (cmd.startswith("_") or cmd.startswith("'")): + cmd = "_" + cmd + try: + # WICHTIG: Mac Rhinos Command-Bar sammelt parallel + # User-Keystrokes (globaler Keyhook). Wenn unsere React- + # Eingabe tippt landet die da auch. ESC clearen sonst + # haben wir doppelten Text und braucht 2x Enter. + Rhino.RhinoApp.SendKeystrokes("\x1b", False) + Rhino.RhinoApp.RunScript(cmd, False) + except Exception as ex: + print("[OBERLEISTE] RunScript-Fehler:", ex) + elif t == "SEND_KEYS": + text = p.get("text") or "" + append_enter = bool(p.get("enter", True)) + try: + # Ebenfalls Buffer zuerst leeren wenn User parallel mitgetippt hat + if text: + Rhino.RhinoApp.SendKeystrokes("\x1b", False) + Rhino.RhinoApp.SendKeystrokes(text, append_enter) + except Exception as ex: + print("[OBERLEISTE] SendKeystrokes-Fehler:", ex) + elif t == "CANCEL_COMMAND": + try: + # Doppel-ESC: einmal um Eingabe-Buffer zu clearen, einmal um + # aktiven Befehl abzubrechen + Rhino.RhinoApp.SendKeystrokes("\x1b", False) + Rhino.RhinoApp.SendKeystrokes("\x1b", False) + except Exception: + pass + elif t == "TOGGLE_RHINO_CMD_LINE": + # Versucht Rhinos eigene Befehlszeile/History zu togglen. + # Mehrere Wege probieren — je nach Version greift einer. + for c in ( + "_-CommandPrompt _Hide _Enter", + "_CommandHistory _Toggle _Enter", + "_-Toolbar _Hide _Commands _Enter", + ): + try: + Rhino.RhinoApp.RunScript(c, False) + except Exception: + pass + + def _send_state(self, force=False): + doc, vp = massstab._active_vp() + info = massstab._compute_scale(doc, vp) + # Massstab-State (Scale, Print-Toggle, DPI) + info["viewMode"] = _get_active_viewport_name() + info["displayMode"] = _get_active_display_mode_name() + # displayModes-Liste nur einmal initial mitsenden — aendert sich kaum + if not getattr(self, "_dm_sent", False): + info["displayModes"] = _list_display_modes() + self._dm_sent = True + # Snap-State + info.update(_get_snap_state()) + # Overrides-State — cached, invalidiert bei TOGGLE_OVERRIDES und + # SET_OVERRIDES_PRESET. Bei manuellen Aenderungen via OVERRIDES-Panel + # bleibt der Cache stale bis zum naechsten Toggle — pragmatischer + # Trade-off, weil die beiden Bridges nicht direkt voneinander wissen. + if self._cached_overrides is None: + try: + cfg = overrides.load_config(doc) + presets = [item.get("name") for item in overrides.list_presets() if item.get("name")] + self._cached_overrides = ( + bool(cfg.get("enabled")), + len(cfg.get("rules") or []), + cfg.get("activePreset"), + tuple(presets), + ) + except Exception: + self._cached_overrides = (False, 0, None, ()) + (info["overridesEnabled"], + info["overridesCount"], + info["overridesActivePreset"], + _presets_tuple) = self._cached_overrides + info["overridesPresets"] = list(_presets_tuple) + # Command-Line State + prompt = _get_command_prompt() + info["cmdPrompt"] = prompt + info["cmdOptions"] = _parse_command_options(prompt) + # Command-Autocomplete-Liste — nur einmal initial schicken (gross) + if not getattr(self, "_commands_sent", False): + info["allCommands"] = self._all_commands + self._commands_sent = True + force = True # Erste Push immer feuern + # Diff-Check: wenn weder Daten noch force, gar nichts schicken + # (dedupe Idle-Ticks ohne Aenderung — spart WebView-ExecuteScript Roundtrip) + sig = ( + info.get("scale"), + info.get("appliedScale"), + info.get("parallel"), + info.get("viewMode"), + info.get("displayMode"), + info.get("ortho"), info.get("gridSnap"), info.get("osnap"), + info.get("showLineweights"), + info["overridesEnabled"], info["overridesCount"], + info.get("overridesActivePreset"), + tuple(info.get("overridesPresets") or ()), + prompt, + ) + if not force and sig == self._last_state_sig: + return + self._last_state_sig = sig + self.send("STATE", info) + + def tick_idle(self): + # Command-Prompt aendert sich oft schnell -> separater Pfad: wenn sich + # der Prompt seit letztem Tick geaendert hat, sofort pushen (ungedrosselt). + cur_prompt = _get_command_prompt() + if cur_prompt != self._last_prompt: + self._last_prompt = cur_prompt + self._send_state(force=True) + self._idle_counter = 0 + return + # Sonst: normaler throttle fuer den restlichen State + self._idle_counter += 1 + if self._idle_counter < massstab._IDLE_THROTTLE: + return + self._idle_counter = 0 + self._send_state(force=False) + + +# --- Listener-Hookup -------------------------------------------------------- + +def _install_listeners(bridge): + flag = "oberleiste_listeners" + sc.sticky["oberleiste_bridge"] = bridge + if sc.sticky.get(flag): + return + + def on_idle(s, e): + b = sc.sticky.get("oberleiste_bridge") + if b is not None: + try: b.tick_idle() + except Exception: pass + + def on_view_change(*args): + b = sc.sticky.get("oberleiste_bridge") + if b is not None: + try: b._send_state(force=True) + except Exception: pass + + Rhino.RhinoApp.Idle += on_idle + Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change + sc.sticky[flag] = True + print("[OBERLEISTE] Listener aktiv") + + +def _bridge_factory(): + b = OberleisteBridge() + _install_listeners(b) + return b + + +panel_base.register_and_open("oberleiste", "OBERLEISTE", PANEL_GUID_STR, _bridge_factory, + icon_spec=("O", "#2f5d54")) diff --git a/rhino/overrides.py b/rhino/overrides.py new file mode 100644 index 0000000..62904ea --- /dev/null +++ b/rhino/overrides.py @@ -0,0 +1,718 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +overrides.py +Engine fuer regelbasierte grafische Overrides (ArchiCAD Graphical Overrides / +Vectorworks Datenvisualisierung). + +Datenmodell (gespeichert als JSON in doc.Strings["dossier_overrides"]): + + { + "enabled": true, + "rules": [ + { + "id": "rule_abc", + "name": "Bestand grau", + "enabled": true, + "condition": { + "type": "layer_name" | "user_string" | "object_name", + "operator": "equals" | "contains" | "starts_with" | "not_equals", + "value": "WAND_BESTAND", + "key": "brandschutz" # nur fuer user_string + }, + "actions": { + "color": "#888888" or null, + "lineweight": 0.25 or null, + "linetype": "Dashed" or null + } + }, + ... (oberste Regel hat hoechste Prioritaet) + ] + } + +Verhalten: +- Mehrere Regeln matchen additiv: Actions aller passenden Regeln werden + kombiniert. Bei Konflikt fuer die selbe Property gewinnt die in der + Liste WEITER OBEN stehende Regel. +- Originalwerte werden in UserStrings pro Objekt gesichert -> reversibel. +- Engine wird via apply_all(doc) / restore_all(doc) gesteuert. +""" +import os +import sys +import json +import Rhino +import System +import System.Drawing as Drawing +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +_STORE_KEY = "dossier_overrides" + +# Globale Presets (cross-doc) — Datei im User-Home +_PRESETS_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel") +_PRESETS_PATH = os.path.join(_PRESETS_DIR, "override_presets.json") + +# UserString-Keys fuer Original-Backups (pro Objekt) +_ORIG_COLOR_SRC = "dossier_or_csrc" +_ORIG_COLOR = "dossier_or_color" +_ORIG_LW_SRC = "dossier_or_lwsrc" +_ORIG_LW = "dossier_or_lw" +_ORIG_LT_SRC = "dossier_or_ltsrc" +_ORIG_LT = "dossier_or_lt" +_OVERRIDDEN = "dossier_or_done" # "1" wenn Object aktuell overridden ist + +# Hatch-Override: Originalwerte werden auf dem Hatch-Objekt selbst gespeichert. +# Link Curve -> Hatch nutzt den FILL_KEY den gestaltung.py setzt. +_GEST_FILL_KEY = "ebenen_fill_hatch_id" # auf Curve +_ORIG_HP = "dossier_or_hatch_pidx" # auf Hatch — original PatternIndex +_ORIG_HS = "dossier_or_hatch_scale" # auf Hatch — original PatternScale +_HATCH_OVERRIDDEN = "dossier_or_hatch_done" # "1" wenn Hatch aktuell overridden + +_FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer +_FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject +_LW_FROM_LAY = Rhino.DocObjects.ObjectPlotWeightSource.PlotWeightFromLayer +_LW_FROM_OBJ = Rhino.DocObjects.ObjectPlotWeightSource.PlotWeightFromObject +_LT_FROM_LAY = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromLayer +_LT_FROM_OBJ = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromObject + + +# --- Daten lesen/schreiben -------------------------------------------------- + +def load_config(doc): + if doc is None: + return {"enabled": False, "rules": []} + try: + raw = doc.Strings.GetValue(_STORE_KEY) + if not raw: + return {"enabled": False, "rules": []} + data = json.loads(raw) + if not isinstance(data, dict): + return {"enabled": False, "rules": []} + data.setdefault("enabled", False) + data.setdefault("rules", []) + return data + except Exception as ex: + print("[OVERRIDES] load_config:", ex) + return {"enabled": False, "rules": []} + + +def save_config(doc, cfg): + if doc is None: return + try: + doc.Strings.SetString(_STORE_KEY, json.dumps(cfg, ensure_ascii=False)) + except Exception as ex: + print("[OVERRIDES] save_config:", ex) + + +# --- Presets (global, cross-doc) ------------------------------------------- + +def _read_presets_file(): + try: + if os.path.isfile(_PRESETS_PATH): + with open(_PRESETS_PATH, "rb") as f: + data = json.loads(f.read().decode("utf-8")) + if isinstance(data, list): + return data + # Migration: alte dict-Form -> list + if isinstance(data, dict) and "presets" in data: + return data.get("presets") or [] + except Exception as ex: + print("[OVERRIDES] read_presets:", ex) + return [] + + +def _write_presets_file(presets): + try: + if not os.path.isdir(_PRESETS_DIR): + os.makedirs(_PRESETS_DIR) + with open(_PRESETS_PATH, "wb") as f: + f.write(json.dumps(presets or [], ensure_ascii=False, indent=2).encode("utf-8")) + return True + except Exception as ex: + print("[OVERRIDES] write_presets:", ex) + return False + + +def list_presets(): + """Liefert Liste von {name, ruleCount}.""" + out = [] + for p in _read_presets_file(): + if not isinstance(p, dict): continue + out.append({ + "name": p.get("name", "(ohne Name)"), + "ruleCount": len(p.get("rules") or []), + }) + return out + + +def save_preset(name, rules): + """Speichert/ueberschreibt Preset mit gegebenem Namen.""" + if not name or not isinstance(name, str): return False + name = name.strip() + if not name: return False + presets = _read_presets_file() + # Existierendes Preset mit gleichem Namen ersetzen + for i, p in enumerate(presets): + if isinstance(p, dict) and p.get("name") == name: + presets[i] = {"name": name, "rules": rules or []} + return _write_presets_file(presets) + # Sonst anhaengen + presets.append({"name": name, "rules": rules or []}) + return _write_presets_file(presets) + + +def load_preset(name): + """Liefert die Rules-Liste eines Presets oder None.""" + for p in _read_presets_file(): + if isinstance(p, dict) and p.get("name") == name: + # Deep-Copy via JSON damit der Aufrufer keine Datei-Daten teilt + return json.loads(json.dumps(p.get("rules") or [])) + return None + + +def delete_preset(name): + presets = _read_presets_file() + new = [p for p in presets if not (isinstance(p, dict) and p.get("name") == name)] + if len(new) == len(presets): return False + return _write_presets_file(new) + + +def set_active_preset(doc, name): + """Aktiviert ein gespeichertes Preset: kopiert dessen Rules ins Doc-Config + und markiert es als activePreset. Wenn name leer/None: aktives Preset + geclear-t, Rules bleiben unveraendert (User waehlt "kein Preset"). Bei + aktivem enabled-Flag wird sofort neu angewendet. True bei Erfolg.""" + if doc is None: return False + cfg = load_config(doc) + if name: + rules = load_preset(name) + if rules is None: + return False + cfg["rules"] = rules + cfg["activePreset"] = name + else: + cfg["activePreset"] = None + save_config(doc, cfg) + if cfg.get("enabled"): + # Erst restore (alte Overrides zuruecknehmen), dann apply mit neuen Rules. + restore_all(doc) + apply_all(doc) + return True + + +def get_active_preset(doc): + """Aktuell aktives Preset-Namen oder None.""" + if doc is None: return None + return load_config(doc).get("activePreset") + + +# --- Helpers ---------------------------------------------------------------- + +def _color_to_hex(c): + if c is None: return None + try: + return "#{:02x}{:02x}{:02x}".format(int(c.R), int(c.G), int(c.B)) + except Exception: + return None + + +def _hex_to_color(h): + if not isinstance(h, str): return Drawing.Color.FromArgb(136, 136, 136) + h = h.strip() + if h.startswith("#"): h = h[1:] + if len(h) != 6: + return Drawing.Color.FromArgb(136, 136, 136) + try: + return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) + except Exception: + return Drawing.Color.FromArgb(136, 136, 136) + + +def _layer_name_for(doc, obj): + try: + idx = obj.Attributes.LayerIndex + if 0 <= idx < doc.Layers.Count: + return doc.Layers[idx].Name or "" + except Exception: + pass + return "" + + +def _layer_full_path_for(doc, obj): + try: + idx = obj.Attributes.LayerIndex + if 0 <= idx < doc.Layers.Count: + return doc.Layers[idx].FullPath or "" + except Exception: + pass + return "" + + +def _user_string_for(obj, key): + try: + v = obj.Attributes.GetUserString(key) + return v if v is not None else "" + except Exception: + return "" + + +def _object_name_for(obj): + try: + return obj.Attributes.Name or "" + except Exception: + return "" + + +def _compare(actual, op, expected): + actual = actual if actual is not None else "" + expected = expected if expected is not None else "" + if op == "equals": return str(actual) == str(expected) + if op == "not_equals": return str(actual) != str(expected) + if op == "contains": return str(expected) in str(actual) + if op == "starts_with": return str(actual).startswith(str(expected)) + if op == "ends_with": return str(actual).endswith(str(expected)) + return False + + +def _match_leaf(doc, obj, condition): + """Evaluates a single leaf condition (layer_name / user_string / object_name).""" + if not isinstance(condition, dict): return False + t = condition.get("type") + op = condition.get("operator") or "equals" + v = condition.get("value") + if t == "layer_name": + return _compare(_layer_name_for(doc, obj), op, v) or \ + _compare(_layer_full_path_for(doc, obj), op, v) + if t == "user_string": + return _compare(_user_string_for(obj, condition.get("key", "")), op, v) + if t == "object_name": + return _compare(_object_name_for(obj), op, v) + return False + + +def _match_rule(doc, obj, rule): + """Evaluates rule. Unterstuetzt zwei Formate: + - Legacy: rule.condition = {single leaf} + - Neu: rule.conditions = [leaf, leaf, ...] + rule.conditionsLogic = "and" | "or" + """ + # Neue Form (Liste) + conds = rule.get("conditions") + if isinstance(conds, list) and conds: + logic = (rule.get("conditionsLogic") or "and").lower() + if logic == "or": + for c in conds: + if _match_leaf(doc, obj, c): + return True + return False + # default: and + for c in conds: + if not _match_leaf(doc, obj, c): + return False + return True + # Legacy single condition + return _match_leaf(doc, obj, rule.get("condition") or {}) + + +# --- Apply / Restore -------------------------------------------------------- + +def _backup_original(attrs): + """Sichert originale Attribute in UserStrings (nur beim ersten Mal).""" + if attrs.GetUserString(_OVERRIDDEN) == "1": + return # bereits gesichert + try: + attrs.SetUserString(_ORIG_COLOR_SRC, str(int(attrs.ColorSource))) + c_hex = _color_to_hex(attrs.ObjectColor) or "#888888" + attrs.SetUserString(_ORIG_COLOR, c_hex) + attrs.SetUserString(_ORIG_LW_SRC, str(int(attrs.PlotWeightSource))) + attrs.SetUserString(_ORIG_LW, "{:.6f}".format(float(attrs.PlotWeight or 0))) + attrs.SetUserString(_ORIG_LT_SRC, str(int(attrs.LinetypeSource))) + attrs.SetUserString(_ORIG_LT, str(int(attrs.LinetypeIndex))) + attrs.SetUserString(_OVERRIDDEN, "1") + except Exception as ex: + print("[OVERRIDES] _backup_original:", ex) + + +def _restore_original(doc, obj): + """Stellt urspruengliche Attribute aus UserStrings wieder her. + Beinhaltet auch das Restoring eines ggf. ueberschriebenen Hatches.""" + a = obj.Attributes + # Hatch separat zuruecksetzen — kann auch ohne Curve-Override + # passiert sein (z.B. wenn Override nur den Pattern aendert) + _restore_hatch(doc, obj) + if a.GetUserString(_OVERRIDDEN) != "1": + return False + try: + new_a = a.Duplicate() + cs = a.GetUserString(_ORIG_COLOR_SRC) + if cs: + new_a.ColorSource = Rhino.DocObjects.ObjectColorSource(int(cs)) + c = a.GetUserString(_ORIG_COLOR) + if c: + new_a.ObjectColor = _hex_to_color(c) + lws = a.GetUserString(_ORIG_LW_SRC) + if lws: + new_a.PlotWeightSource = Rhino.DocObjects.ObjectPlotWeightSource(int(lws)) + lw = a.GetUserString(_ORIG_LW) + if lw: + try: new_a.PlotWeight = float(lw) + except Exception: pass + lts = a.GetUserString(_ORIG_LT_SRC) + if lts: + new_a.LinetypeSource = Rhino.DocObjects.ObjectLinetypeSource(int(lts)) + lt = a.GetUserString(_ORIG_LT) + if lt: + try: new_a.LinetypeIndex = int(lt) + except Exception: pass + # Backup-Marker entfernen + for k in (_ORIG_COLOR_SRC, _ORIG_COLOR, _ORIG_LW_SRC, _ORIG_LW, + _ORIG_LT_SRC, _ORIG_LT, _OVERRIDDEN): + new_a.SetUserString(k, "") + doc.Objects.ModifyAttributes(obj, new_a, True) + return True + except Exception as ex: + print("[OVERRIDES] _restore_original:", ex) + return False + + +def _compose_overrides(doc, obj, rules): + """Sammelt Actions aller matchenden Regeln. Bei Konflikten gewinnt die + Regel die WEITER OBEN in der Liste steht (= niedrigerer Index).""" + composed = {} + for rule in rules: + if not rule.get("enabled", True): continue + if not _match_rule(doc, obj, rule): continue + for prop, val in (rule.get("actions") or {}).items(): + if val is None or val == "": continue + if prop not in composed: + composed[prop] = val + return composed + + +def _find_linked_hatch(doc, curve_obj): + """Findet den via gestaltung verlinkten Hatch zur Curve (oder None).""" + try: + hid_s = curve_obj.Attributes.GetUserString(_GEST_FILL_KEY) + if not hid_s: return None + h = doc.Objects.FindId(System.Guid(hid_s)) + if h is None or h.IsDeleted: return None + return h + except Exception: + return None + + +def _apply_hatch_override(doc, curve_obj, pattern_name, scale_val): + """Modifiziert den verlinkten Hatch der Curve. Original wird auf dem + Hatch in UserStrings gesichert. Liefert True bei Aenderung. + + Wenn keine Hatch existiert: stiller No-op (User soll erst via Gestaltung + eine Basis-Hatch anlegen — Overrides modifizieren, erzeugen nicht).""" + h = _find_linked_hatch(doc, curve_obj) + if h is None: return False + try: + hg = h.Geometry + ha = h.Attributes + # Backup einmalig sichern + if ha.GetUserString(_HATCH_OVERRIDDEN) != "1": + try: + ha.SetUserString(_ORIG_HP, str(int(hg.PatternIndex))) + ha.SetUserString(_ORIG_HS, "{:.6f}".format(float(hg.PatternScale))) + ha.SetUserString(_HATCH_OVERRIDDEN, "1") + doc.Objects.ModifyAttributes(h, ha, True) + except Exception as ex: + print("[OVERRIDES] hatch backup:", ex) + # Pattern wechseln (Geometrie neu erzeugen — PatternIndex ist read-only) + new_pidx = hg.PatternIndex + if pattern_name: + try: + idx = doc.HatchPatterns.Find(pattern_name, True) + if idx >= 0: + new_pidx = idx + except Exception: pass + new_scale = float(scale_val) if scale_val else float(hg.PatternScale) + try: + # Hatch-Geometrie neu instanzieren (PatternIndex/Scale aendern direkt) + new_hg = hg.Duplicate() + try: + new_hg.PatternIndex = new_pidx + except Exception: pass + try: + new_hg.PatternScale = new_scale + except Exception: pass + doc.Objects.Replace(h.Id, new_hg) + return True + except Exception as ex: + print("[OVERRIDES] hatch replace:", ex) + return False + except Exception as ex: + print("[OVERRIDES] _apply_hatch_override:", ex) + return False + + +def _restore_hatch(doc, curve_obj): + """Stellt Hatch-Pattern und -Scale aus dem Backup wieder her.""" + h = _find_linked_hatch(doc, curve_obj) + if h is None: return False + a = h.Attributes + if a.GetUserString(_HATCH_OVERRIDDEN) != "1": return False + try: + orig_pidx_s = a.GetUserString(_ORIG_HP) + orig_scale_s = a.GetUserString(_ORIG_HS) + hg = h.Geometry.Duplicate() + if orig_pidx_s: + try: hg.PatternIndex = int(orig_pidx_s) + except Exception: pass + if orig_scale_s: + try: hg.PatternScale = float(orig_scale_s) + except Exception: pass + doc.Objects.Replace(h.Id, hg) + # Backup-Marker entfernen + h2 = doc.Objects.FindId(h.Id) + if h2 is not None: + new_a = h2.Attributes.Duplicate() + for k in (_ORIG_HP, _ORIG_HS, _HATCH_OVERRIDDEN): + new_a.SetUserString(k, "") + doc.Objects.ModifyAttributes(h2, new_a, True) + return True + except Exception as ex: + print("[OVERRIDES] _restore_hatch:", ex) + return False + + +def _apply_to_object(doc, obj, overrides): + """Setzt die Override-Werte am Objekt. Sichert vorher Originale.""" + if not overrides: return False + a = obj.Attributes + _backup_original(a) + new_a = a.Duplicate() + changed = False + if "color" in overrides: + col = _hex_to_color(overrides["color"]) + new_a.ColorSource = _FROM_OBJECT + new_a.ObjectColor = col + # Plot-Color mitspiegeln (sonst druckt's wieder in Layerfarbe) + try: + new_a.PlotColorSource = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject + new_a.PlotColor = col + except Exception: pass + changed = True + if "lineweight" in overrides: + try: + new_a.PlotWeightSource = _LW_FROM_OBJ + new_a.PlotWeight = float(overrides["lineweight"]) + changed = True + except Exception: pass + if "linetype" in overrides: + try: + idx = doc.Linetypes.Find(overrides["linetype"], True) + if idx >= 0: + new_a.LinetypeSource = _LT_FROM_OBJ + new_a.LinetypeIndex = idx + changed = True + except Exception: pass + if changed: + try: + doc.Objects.ModifyAttributes(obj, new_a, True) + except Exception as ex: + print("[OVERRIDES] apply ModifyAttributes:", ex) + # Hatch-Override (separater Pfad, modifiziert das verlinkte Hatch) + if "hatchPattern" in overrides or "hatchScale" in overrides: + if _apply_hatch_override(doc, obj, + overrides.get("hatchPattern"), + overrides.get("hatchScale")): + changed = True + return changed + + +def apply_all(doc): + """Wendet alle aktiven Regeln auf alle Objekte im Doc an. + Objekte die NICHT (mehr) matchen werden auf Originale zurueckgesetzt.""" + if doc is None: return 0, 0 + cfg = load_config(doc) + if not cfg.get("enabled"): return 0, 0 + rules = cfg.get("rules") or [] + if not rules: return 0, 0 + n_applied = 0 + n_restored = 0 + _set_applying(True) + try: + for obj in doc.Objects: + if obj is None or obj.IsDeleted: continue + ovs = _compose_overrides(doc, obj, rules) + if ovs: + if _apply_to_object(doc, obj, ovs): + n_applied += 1 + else: + # Kein Match aber war evtl. vorher overridden -> restore + if obj.Attributes.GetUserString(_OVERRIDDEN) == "1": + if _restore_original(doc, obj): + n_restored += 1 + try: doc.Views.Redraw() + except Exception: pass + print("[OVERRIDES] apply_all: {} applied, {} restored".format(n_applied, n_restored)) + except Exception as ex: + print("[OVERRIDES] apply_all:", ex) + finally: + _set_applying(False) + return n_applied, n_restored + + +def restore_all(doc): + """Stellt alle Originale wieder her (Overrides aus).""" + if doc is None: return 0 + n = 0 + _set_applying(True) + try: + for obj in doc.Objects: + if obj is None or obj.IsDeleted: continue + had_attr_override = (obj.Attributes.GetUserString(_OVERRIDDEN) == "1") + # _restore_original kuemmert sich auch um den verlinkten Hatch — + # auch wenn die Curve selbst keinen Attribut-Override hatte. + if had_attr_override: + if _restore_original(doc, obj): + n += 1 + else: + # Vielleicht nur Hatch-Override + if _restore_hatch(doc, obj): + n += 1 + try: doc.Views.Redraw() + except Exception: pass + print("[OVERRIDES] restore_all: {} Objekte".format(n)) + except Exception as ex: + print("[OVERRIDES] restore_all:", ex) + finally: + _set_applying(False) + return n + + +def set_enabled(doc, enabled): + """Master-Toggle: an -> apply_all, aus -> restore_all + Config-Flag setzen.""" + cfg = load_config(doc) + cfg["enabled"] = bool(enabled) + save_config(doc, cfg) + if enabled: + apply_all(doc) + else: + restore_all(doc) + + +def update_rules(doc, rules, enabled=None): + """Schreibt eine neue Regel-Liste. Wenn enabled vorher an war, wird + nach dem Speichern apply_all (mit Restore-cleanup) ausgefuehrt. + Manuelle Aenderungen an den Rules clearen den activePreset — sonst + behauptet das Topbar-Dropdown weiter, das alte Preset sei aktiv obwohl + die Rules davon driften (Variante C: Preset ist read-only Snapshot).""" + cfg = load_config(doc) + if enabled is not None: + cfg["enabled"] = bool(enabled) + cfg["rules"] = rules or [] + cfg["activePreset"] = None + save_config(doc, cfg) + if cfg.get("enabled"): + # Erst alles zuruecksetzen, dann neu anwenden — sonst koennten alte + # Overrides "kleben" wenn die neue Regelmenge sie nicht mehr enthaelt. + restore_all(doc) + apply_all(doc) + + +# --- Live-Update via Doc-Events -------------------------------------------- + +def _is_applying(): + return bool(sc.sticky.get("overrides_applying")) + + +def _set_applying(v): + sc.sticky["overrides_applying"] = bool(v) + + +def _apply_to_single_object(doc, obj): + """Re-evaluate Overrides fuer ein einzelnes Objekt. Aufgerufen von den + Event-Handlern bei neu/geaenderten Objekten.""" + if doc is None or obj is None: return + cfg = load_config(doc) + if not cfg.get("enabled"): return + rules = cfg.get("rules") or [] + if not rules: + # Engine aus oder keine Regeln -> wenn vorher overridden, restore + try: + if obj.Attributes.GetUserString(_OVERRIDDEN) == "1": + _restore_original(doc, obj) + except Exception: pass + return + try: + ovs = _compose_overrides(doc, obj, rules) + if ovs: + _apply_to_object(doc, obj, ovs) + elif obj.Attributes.GetUserString(_OVERRIDDEN) == "1": + _restore_original(doc, obj) + except Exception as ex: + print("[OVERRIDES] live single-apply:", ex) + + +def install_listeners(): + """Hookt einmalig Rhino-Events fuer Live-Update. + Idempotent via sticky-Flag.""" + if sc.sticky.get("overrides_listeners"): + return + + def on_add(s, e): + if _is_applying(): return + try: + doc = getattr(e, "TheDoc", None) or Rhino.RhinoDoc.ActiveDoc + obj = getattr(e, "TheObject", None) + if not obj or not doc: return + _set_applying(True) + try: + _apply_to_single_object(doc, obj) + finally: + _set_applying(False) + except Exception as ex: + print("[OVERRIDES] on_add:", ex) + _set_applying(False) + + def on_replace(s, e): + # Wird auch von ModifyAttributes gefeuert -> Guard + if _is_applying(): return + try: + doc = getattr(e, "TheDoc", None) or Rhino.RhinoDoc.ActiveDoc + obj = getattr(e, "NewRhinoObject", None) or getattr(e, "TheObject", None) + if not obj or not doc: return + _set_applying(True) + try: + _apply_to_single_object(doc, obj) + finally: + _set_applying(False) + except Exception as ex: + print("[OVERRIDES] on_replace:", ex) + _set_applying(False) + + def on_layer_table(s, e): + # Layer geaendert (Name, Properties, ...) — Regeln mit layer_name + # koennten andere Matches haben. Vollstaendiges Reapply. + if _is_applying(): return + try: + doc = getattr(e, "Document", None) or Rhino.RhinoDoc.ActiveDoc + cfg = load_config(doc) + if not cfg.get("enabled"): return + _set_applying(True) + try: + restore_all(doc) + apply_all(doc) + finally: + _set_applying(False) + except Exception as ex: + print("[OVERRIDES] on_layer_table:", ex) + _set_applying(False) + + try: + Rhino.RhinoDoc.AddRhinoObject += on_add + Rhino.RhinoDoc.ReplaceRhinoObject += on_replace + Rhino.RhinoDoc.LayerTableEvent += on_layer_table + except Exception as ex: + print("[OVERRIDES] install_listeners:", ex) + return + + sc.sticky["overrides_listeners"] = True + print("[OVERRIDES] Live-Update Listener aktiv (Add/Replace/LayerTable)") diff --git a/rhino/overrides_panel.py b/rhino/overrides_panel.py new file mode 100644 index 0000000..9592b5e --- /dev/null +++ b/rhino/overrides_panel.py @@ -0,0 +1,226 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +overrides_panel.py +OVERRIDES-Panel: Rule-Editor fuer grafische Overrides. +Liest/schreibt rhino/overrides.py-Engine. +""" +import os +import sys +import uuid +import Rhino +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import panel_base +import overrides + +PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62" + + +def _list_layer_names(doc): + out = [] + try: + for layer in doc.Layers: + if layer is None or layer.IsDeleted: continue + try: + out.append({"name": layer.Name, "fullPath": layer.FullPath or layer.Name}) + except Exception: + pass + except Exception: + pass + return out + + +def _list_linetypes(doc): + out = [] + try: + for lt in doc.Linetypes: + try: + if lt.Name and not lt.IsDeleted: + out.append(lt.Name) + except Exception: + pass + except Exception: + pass + return out + + +def _list_hatch_patterns(doc): + out = [] + try: + for i in range(doc.HatchPatterns.Count): + try: + hp = doc.HatchPatterns[i] + if hp and hp.Name: + out.append(hp.Name) + except Exception: + pass + except Exception: + pass + return out + + +def _payload(doc): + cfg = overrides.load_config(doc) + return { + "enabled": bool(cfg.get("enabled")), + "rules": cfg.get("rules") or [], + "layers": _list_layer_names(doc), + "linetypes": _list_linetypes(doc), + "hatchPatterns": _list_hatch_patterns(doc), + "presets": overrides.list_presets(), + "activePreset": cfg.get("activePreset"), + } + + +class OverridesBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "overrides") + + def _on_ready(self): + self._send_state() + + def _send_state(self): + doc = Rhino.RhinoDoc.ActiveDoc + self.send("STATE", _payload(doc)) + # Oberleiste mit-informieren: deren Topbar zeigt + # Toggle + Preset-Dropdown, das vom selben State abhaengt. + # Cache invalidieren, dann force-send, sonst sieht die Topbar + # neue Rules/Presets erst beim naechsten Toggle. + try: + b = sc.sticky.get("oberleiste_bridge") + if b is not None: + b._cached_overrides = None + b._send_state(force=True) + except Exception as ex: + print("[OVERRIDES] notify oberleiste:", ex) + + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): p = {} + doc = Rhino.RhinoDoc.ActiveDoc + + if t == "READY" or t == "REQUEST_STATE": + self._on_ready() + elif t == "SET_ENABLED": + overrides.set_enabled(doc, bool(p.get("enabled"))) + self._send_state() + elif t == "ADD_RULE": + cfg = overrides.load_config(doc) + rule = p.get("rule") or {} + if not rule.get("id"): + rule["id"] = "rule_" + uuid.uuid4().hex[:8] + rule.setdefault("name", "Neue Regel") + rule.setdefault("enabled", True) + rule.setdefault("condition", {"type": "layer_name", "operator": "equals", "value": ""}) + rule.setdefault("actions", {}) + # Neue Regel oben einfuegen (= hoechste Prioritaet) + rules = cfg.get("rules") or [] + rules.insert(0, rule) + overrides.update_rules(doc, rules, cfg.get("enabled")) + self._send_state() + elif t == "UPDATE_RULE": + cfg = overrides.load_config(doc) + rid = p.get("id") + patch = p.get("rule") or {} + rules = cfg.get("rules") or [] + for i, r in enumerate(rules): + if r.get("id") == rid: + rules[i] = patch + break + overrides.update_rules(doc, rules, cfg.get("enabled")) + self._send_state() + elif t == "DELETE_RULE": + cfg = overrides.load_config(doc) + rid = p.get("id") + rules = [r for r in (cfg.get("rules") or []) if r.get("id") != rid] + overrides.update_rules(doc, rules, cfg.get("enabled")) + self._send_state() + elif t == "REORDER_RULES": + cfg = overrides.load_config(doc) + order = p.get("order") or [] + by_id = {r.get("id"): r for r in (cfg.get("rules") or [])} + new_rules = [by_id[i] for i in order if i in by_id] + # Verbleibende (falls Liste inkonsistent) hinten anhaengen + for r in (cfg.get("rules") or []): + if r not in new_rules: + new_rules.append(r) + overrides.update_rules(doc, new_rules, cfg.get("enabled")) + self._send_state() + elif t == "DUPLICATE_RULE": + cfg = overrides.load_config(doc) + rid = p.get("id") + rules = cfg.get("rules") or [] + for i, r in enumerate(rules): + if r.get("id") == rid: + import json + clone = json.loads(json.dumps(r, ensure_ascii=False)) + clone["id"] = "rule_" + uuid.uuid4().hex[:8] + clone["name"] = (r.get("name", "Regel") + " Kopie") + rules.insert(i + 1, clone) + break + overrides.update_rules(doc, rules, cfg.get("enabled")) + self._send_state() + elif t == "REAPPLY": + if overrides.load_config(doc).get("enabled"): + overrides.restore_all(doc) + overrides.apply_all(doc) + self._send_state() + elif t == "CLEAR_RULES": + # Alle Regeln entfernen und activePreset clearen — wird vom + # Topbar/Kombination-Dropdown beim Wechsel auf "— neu / keine —" + # gefeuert, damit der Editor wirklich leer ist. + cfg = overrides.load_config(doc) + overrides.update_rules(doc, [], cfg.get("enabled")) + self._send_state() + + # --- Presets (cross-doc) ---------------------------------------- + elif t == "SAVE_PRESET": + name = (p.get("name") or "").strip() + if name: + cfg = overrides.load_config(doc) + overrides.save_preset(name, cfg.get("rules") or []) + self._send_state() + elif t == "LOAD_PRESET": + name = (p.get("name") or "").strip() + mode = p.get("mode") or "replace" # 'replace' oder 'append' + if mode == "replace": + # set_active_preset macht alles richtig: Rules ersetzen, + # activePreset = name, ggf. neu anwenden. + overrides.set_active_preset(doc, name) + else: + # Append-Mode: bestehende + Preset-Rules. activePreset wird + # in update_rules auf None gesetzt — passt, weil's eine + # Mischung ist, kein einzelnes Preset mehr. + rules = overrides.load_preset(name) + if rules is not None: + cfg = overrides.load_config(doc) + new_rules = list(cfg.get("rules") or []) + list(rules) + overrides.update_rules(doc, new_rules, cfg.get("enabled")) + self._send_state() + elif t == "DELETE_PRESET": + name = (p.get("name") or "").strip() + if name: + overrides.delete_preset(name) + self._send_state() + + +def _bridge_factory(): + b = OverridesBridge() + try: overrides.install_listeners() + except Exception as ex: print("[OVERRIDES] install_listeners:", ex) + # Bridge im sticky ablegen, damit andere Panels (z.B. Oberleiste) sie + # bei Cross-Panel-Updates erreichen koennen. + sc.sticky["overrides_bridge"] = b + return b + + +panel_base.register_and_open("overrides", "OVERRIDES", PANEL_GUID_STR, _bridge_factory, + icon_spec=("V", "#b5621e"), + min_size=(720, 560)) diff --git a/rhino/panel_base.py b/rhino/panel_base.py new file mode 100644 index 0000000..0eff50f --- /dev/null +++ b/rhino/panel_base.py @@ -0,0 +1,499 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +panel_base.py +Geteilte Infrastruktur fuer dockbare Rhino-Panels mit React-WebView. +Wird von rhinopanel.py (EBENEN) und gestaltung.py (GESTALTUNG) verwendet. +""" +import os +import re +import json +import Rhino +import Rhino.UI as RhinoUI +import Eto.Forms as forms +import Eto.Drawing as drawing +import System +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_DIST = os.path.join(_HERE, "..", "dist", "index.html") + + +# --- Legacy-Migration: traite_* / pause_* -> dossier_* ---------------------- +# +# Historisch hatte das Plugin nacheinander die Praefixe "traite_" und "pause_" +# bevor es zu "dossier_" wurde. doc.Strings, Layer-UserStrings und +# Object-UserStrings werden einmalig pro Doc nach "dossier_*" kopiert. +# Idempotent — bestehende dossier_*-Werte werden nicht ueberschrieben. + +_LEGACY_DOC_KEYS = ( + "zeichnungsebenen", "ebenen", "active_id", "active_code", + "ausschnitte", "ausschnitt_folders", "layer_presets", + "user_scale", "dpi", + "show_lineweights", "plotweight_orig", "hatch_scale_orig", + "clipping_plane", +) +_LEGACY_LAYER_USER_KEYS = ("id", "code") +_LEGACY_OBJECT_USER_KEYS = ("clipping_plane", "plotweight_orig", "hatch_scale_orig") +_LEGACY_PREFIXES = ("traite_", "pause_") +_MIGRATE_FLAG = "dossier_migrated_v2" # neuer Flag — laeuft auch auf Docs die nur traite->pause hatten + + +def migrate_to_dossier(doc): + """Migriert einmalig pro Document alle traite_*- und pause_*-Keys zu + dossier_*. No-op wenn schon migriert (per doc.Strings-Flag erkannt).""" + if doc is None: + return + try: + if doc.Strings.GetValue(_MIGRATE_FLAG): + return + except Exception: + return + moved_ds = 0 + # 1) doc.Strings + try: + for suffix in _LEGACY_DOC_KEYS: + new = "dossier_" + suffix + try: + if doc.Strings.GetValue(new): + continue # Dossier-Variante vorhanden -> nicht ueberschreiben + for prefix in _LEGACY_PREFIXES: + old_v = doc.Strings.GetValue(prefix + suffix) + if old_v: + doc.Strings.SetString(new, old_v) + moved_ds += 1 + break + except Exception: + pass + except Exception as ex: + print("[DOSSIER] migrate doc.Strings:", ex) + # 2) Layer-UserStrings + moved_layer = 0 + try: + for layer in doc.Layers: + if layer is None or layer.IsDeleted: + continue + for suffix in _LEGACY_LAYER_USER_KEYS: + try: + if layer.GetUserString("dossier_" + suffix): + continue + for prefix in _LEGACY_PREFIXES: + v = layer.GetUserString(prefix + suffix) + if v: + layer.SetUserString("dossier_" + suffix, v) + moved_layer += 1 + break + except Exception: + pass + except Exception as ex: + print("[DOSSIER] migrate layers:", ex) + # 3) Object-UserStrings + moved_obj = 0 + try: + for obj in doc.Objects: + if obj is None or obj.IsDeleted: + continue + attrs = obj.Attributes + need_apply = False + a2 = None + for suffix in _LEGACY_OBJECT_USER_KEYS: + try: + if attrs.GetUserString("dossier_" + suffix): + continue + for prefix in _LEGACY_PREFIXES: + v = attrs.GetUserString(prefix + suffix) + if v: + if a2 is None: + a2 = attrs.Duplicate() + a2.SetUserString("dossier_" + suffix, v) + need_apply = True + moved_obj += 1 + break + except Exception: + pass + if need_apply and a2 is not None: + try: + doc.Objects.ModifyAttributes(obj, a2, True) + except Exception: + pass + except Exception as ex: + print("[DOSSIER] migrate objects:", ex) + # Flag setzen damit die Migration nicht erneut laeuft + try: + doc.Strings.SetString(_MIGRATE_FLAG, "1") + except Exception: + pass + if moved_ds or moved_layer or moved_obj: + print("[DOSSIER] Migration: doc.Strings={} Layer-UserStrings={} Object-UserStrings={}".format( + moved_ds, moved_layer, moved_obj)) + + +# --- Bridge ----------------------------------------------------------------- + +class BaseBridge(object): + """ + Basis-Bridge mit Chunk-Zusammenbau. + Subklassen ueberschreiben handle(data) und optional _on_ready(). + """ + def __init__(self, mode): + self._wv = None + self._mode = mode + self._chunks = {} + self._chunk_total = 0 + + def set_webview(self, wv): + self._wv = wv + + def handle_raw(self, raw_str): + if not raw_str: + return + try: + data = json.loads(raw_str) + except Exception as ex: + print("[{}] JSON-Fehler: {}".format(self._mode.upper(), ex)) + return + if not isinstance(data, dict): + return + if "_chunk" in data: + c = data["_chunk"] + self._chunks[int(c["i"])] = data["d"] + self._chunk_total = int(c["n"]) + if len(self._chunks) == self._chunk_total: + full = "".join(self._chunks[k] for k in sorted(self._chunks.keys())) + self._chunks = {} + self._chunk_total = 0 + try: + self.handle(json.loads(full)) + except Exception as ex: + import traceback + print("[{}] Chunk-Reassembly: {}".format(self._mode.upper(), ex)) + print("[{}] Traceback:\n{}".format(self._mode.upper(), traceback.format_exc())) + else: + self.handle(data) + + def handle(self, data): + """Override. Default behandelt nur READY.""" + if not isinstance(data, dict): + return + if data.get("type") == "READY": + self._on_ready() + + def _on_ready(self): + """Subklasse sendet hier den initialen State.""" + pass + + def send(self, msg_type, payload=None): + if self._wv is None: + return + # ensure_ascii=False umgeht Rhinos buggy json/encoder.py + # (s.decode('utf-8') auf .NET-Strings mit Umlauten -> CP1252-Codec-Fehler) + data = json.dumps({"type": msg_type, "payload": payload or {}}, ensure_ascii=False) + try: + self._wv.ExecuteScript( + "if(window.onRhinoMessage)window.onRhinoMessage({});".format(data) + ) + except Exception: + pass + + +# --- HTML laden ------------------------------------------------------------- + +def load_inline(wv, mode): + """Laedt dist/index.html inline und injiziert window.PANEL_MODE.""" + if not os.path.exists(_DIST): + print("[{}] dist nicht gefunden".format(mode.upper())) + return + dist_dir = os.path.dirname(_DIST) + with open(_DIST, "rb") as f: + html = f.read().decode("utf-8") + + mode_script = ''.format(mode) + if "" in html: + html = html.replace("", mode_script + "") + else: + html = mode_script + html + + def inline_css(m): + p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep)) + with open(p, "rb") as f2: + return u"" + + def inline_js(m): + p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep)) + with open(p, "rb") as f2: + content = f2.read().decode("utf-8") + return (u'') + + html = re.sub(r']+href="(\./assets/[^"]+\.css)"[^>]*/?>', inline_css, html) + html = re.sub(r']+src="(\./assets/[^"]+\.js)"[^>]*>', inline_js, html) + wv.LoadHtml(html) + + +def attach_webview(panel, bridge, mode): + wv = forms.WebView() + bridge.set_webview(wv) + panel.Content = wv + + def on_title(s, e): + title = e.Title or "" + if not title.startswith("RHINOMSG::"): + return + try: + bridge.handle_raw(title[10:]) + except Exception as ex: + print("[{}] Message-Fehler: {}".format(mode.upper(), ex)) + finally: + try: + wv.ExecuteScript("document.title='{}';".format(mode.upper())) + except Exception: + pass + + def on_loaded(s, e): + try: + wv.ExecuteScript("window.RHINO_MODE=true;") + except Exception: + pass + + def on_idle(s, e): + Rhino.RhinoApp.Idle -= on_idle + try: + load_inline(wv, mode) + except Exception as ex: + print("[{}] Inline-Fehler: {}".format(mode.upper(), ex)) + + wv.DocumentTitleChanged += on_title + wv.DocumentLoaded += on_loaded + Rhino.RhinoApp.Idle += on_idle + + +# --- Dynamic .NET Type ------------------------------------------------------ + +def create_dockable_type(guid_str, type_name, assembly_name): + """Baut einen echten CLR-Typ mit [Guid] fuer Rhinos RegisterPanel.""" + import clr + import System.Reflection as SR + import System.Reflection.Emit as SRE + + panel_base = clr.GetClrType(forms.Panel) + action_t = clr.GetClrType(System.Action[forms.Panel]) + + asm = SRE.AssemblyBuilder.DefineDynamicAssembly( + SR.AssemblyName(assembly_name), + SRE.AssemblyBuilderAccess.Run + ) + mod = asm.DefineDynamicModule(assembly_name) + + tb = mod.DefineType( + type_name, + SR.TypeAttributes.Public | SR.TypeAttributes.Class, + panel_base + ) + + guid_attr_t = clr.GetClrType(System.Runtime.InteropServices.GuidAttribute) + guid_ctor = guid_attr_t.GetConstructor( + System.Array[System.Type]([clr.GetClrType(System.String)]) + ) + tb.SetCustomAttribute(SRE.CustomAttributeBuilder( + guid_ctor, System.Array[System.Object]([guid_str]) + )) + + cb_field = tb.DefineField( + "_callback", action_t, + SR.FieldAttributes.Public | SR.FieldAttributes.Static + ) + + ctor = tb.DefineConstructor( + SR.MethodAttributes.Public, + SR.CallingConventions.Standard, + System.Type.EmptyTypes + ) + il = ctor.GetILGenerator() + lbl = il.DefineLabel() + + base_ctor = panel_base.GetConstructor(System.Type.EmptyTypes) + il.Emit(SRE.OpCodes.Ldarg_0) + il.Emit(SRE.OpCodes.Call, base_ctor) + + il.Emit(SRE.OpCodes.Ldsfld, cb_field) + il.Emit(SRE.OpCodes.Brfalse_S, lbl) + il.Emit(SRE.OpCodes.Ldsfld, cb_field) + il.Emit(SRE.OpCodes.Ldarg_0) + il.Emit(SRE.OpCodes.Callvirt, action_t.GetMethod("Invoke")) + il.MarkLabel(lbl) + il.Emit(SRE.OpCodes.Ret) + + return tb.CreateType() + + +def _hex_rgb(h): + h = (h or "888888").lstrip("#") + return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) + + +_ICON_CACHE_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel/icons") + + +def make_panel_icon(letter, bg_hex): + """Erzeugt ein Icon (32x32) mit farbigem Quadrat + Buchstabe. + Schreibt es als PNG-Datei auf Disk und laedt es via Eto.Drawing.Icon(path) + — das ist der zuverlaessigste Weg auf Mac Rhino. + """ + try: + size = 32 # 32x32 fuer Retina (wird auf 16pt skaliert dargestellt) + bmp = drawing.Bitmap(size, size, drawing.PixelFormat.Format32bppRgba) + g = drawing.Graphics(bmp) + try: + try: g.AntiAlias = True + except Exception: pass + r, gg, bl = _hex_rgb(bg_hex) + bg = drawing.Color.FromArgb(r, gg, bl, 255) + g.FillRectangle(bg, 0, 0, size, size) + try: + font = drawing.Font(drawing.FontFamilies.Sans, 18, drawing.FontStyle.Bold) + except Exception: + font = drawing.Font("Helvetica", 18, drawing.FontStyle.Bold) + try: + text_size = g.MeasureString(font, letter) + tx = (size - text_size.Width) / 2 + ty = (size - text_size.Height) / 2 + except Exception: + tx, ty = size * 0.18, size * 0.12 + g.DrawText(font, drawing.Colors.White, float(tx), float(ty), letter) + finally: + g.Dispose() + # PNG auf Disk schreiben — zuverlaessig fuer Mac Eto.Drawing.Icon + try: + if not os.path.isdir(_ICON_CACHE_DIR): + os.makedirs(_ICON_CACHE_DIR) + safe = re.sub(r"[^A-Za-z0-9]", "_", letter) + path = os.path.join(_ICON_CACHE_DIR, "icon_{}_{}.png".format( + safe, bg_hex.lstrip("#"))) + bmp.Save(path, drawing.ImageFormat.Png) + except Exception as ex: + print("[panel_base] Icon-Save:", ex) + path = None + # 1. Versuch: Icon aus Datei-Pfad + if path and os.path.isfile(path): + try: + return drawing.Icon(path) + except Exception as ex: + print("[panel_base] Icon(path) fehlgeschlagen:", ex) + # 2. Versuch: Icon(scale, bitmap) + try: + return drawing.Icon(1.0, bmp) + except Exception: pass + # 3. Versuch: Icon(bitmap) + try: + return drawing.Icon(bmp) + except Exception: pass + # 4. Fallback: einfach das Bitmap zurueck (Rhino akzeptiert ggf. das auch) + return bmp + except Exception as ex: + print("[panel_base] Icon-Erstellung fehlgeschlagen:", ex) + return None + + +def find_plugin(): + try: + installed = Rhino.PlugIns.PlugIn.GetInstalledPlugIns() + for guid in installed.Keys: + name = str(installed[guid]) + if any(k in name for k in ["RhinoCode", "Scripting", "Python", "Script"]): + p = Rhino.PlugIns.PlugIn.Find(guid) + if p is not None: + return p + except Exception as ex: + print("[panel_base] Plugin-Suche:", ex) + return None + + +def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, min_size=None): + """ + Registriert (falls noetig) und oeffnet ein Panel. + bridge_factory: callable() -> BaseBridge Subklasse + icon_spec: (letter, hex_bg) zum Generieren eines Tab-Icons, oder None. + min_size: (width, height) als Tuple — Panel-MinimumSize beim Erstellen. + Bei docked Panels wirkt's als Hint, bei float Panels als + tatsaechliche Startgroesse. + """ + sticky_reg = "panel_registered_" + mode + sticky_guid = "panel_guid_" + mode + + if not sc.sticky.get(sticky_reg): + plugin = find_plugin() + if plugin is None: + print("[{}] Plugin nicht gefunden".format(mode.upper())) + return + try: + type_name = "DynPanel_" + mode + asm_name = "RhinoPanelDyn_" + mode + dyn_type = create_dockable_type(guid_str, type_name, asm_name) + + def on_created(panel): + # MinimumSize setzen damit Rhino dem Panel bei Auto-Dock / + # Float genug Platz gibt (sonst spawned es schmal). + if min_size is not None: + try: + panel.MinimumSize = drawing.Size(int(min_size[0]), int(min_size[1])) + except Exception as ex: + print("[{}] MinimumSize konnte nicht gesetzt werden: {}".format(mode.upper(), ex)) + # Auf einigen Eto-Versionen gibt es zusaetzlich Size/ClientSize + for attr in ("Size", "ClientSize"): + try: + if hasattr(panel, attr): + setattr(panel, attr, drawing.Size(int(min_size[0]), int(min_size[1]))) + except Exception: pass + bridge = bridge_factory() + attach_webview(panel, bridge, mode) + + dyn_type.GetField("_callback").SetValue( + None, System.Action[forms.Panel](on_created) + ) + icon = None + if icon_spec: + try: + icon = make_panel_icon(icon_spec[0], icon_spec[1]) + except Exception as ex: + print("[{}] Icon-Erstellung uebersprungen: {}".format(mode.upper(), ex)) + icon = None + registered = False + registered_with_icon = False + # Erst mit Icon versuchen, dann stillschweigend ohne (Mac Rhino-Panels + # akzeptieren auf manchen Versionen nur System.Drawing.Icon, das auf + # Mac nicht verfuegbar ist - die Registrierung ohne Icon ist OK). + attempts = [(icon, True)] if icon is not None else [] + attempts.append((None, False)) + for arg, with_icon in attempts: + try: + RhinoUI.Panels.RegisterPanel(plugin, dyn_type, caption, arg) + registered = True + registered_with_icon = with_icon + if with_icon: + print("[{}] Panel mit Icon registriert ({})".format( + mode.upper(), type(arg).__name__)) + break + except Exception as ex: + if with_icon: + print("[{}] RegisterPanel mit Icon fehlgeschlagen: {}".format( + mode.upper(), ex)) + else: + print("[{}] RegisterPanel fehlgeschlagen: {}".format( + mode.upper(), ex)) + if registered and not registered_with_icon and icon is not None: + print("[{}] Panel ohne Icon registriert (Fallback)".format(mode.upper())) + if not registered: + return + sc.sticky[sticky_reg] = True + sc.sticky[sticky_guid] = System.Guid(guid_str) + print("[{}] Panel registriert".format(mode.upper())) + except Exception as ex: + print("[{}] Registrierung fehlgeschlagen: {}".format(mode.upper(), ex)) + return + + try: + guid = sc.sticky.get(sticky_guid, System.Guid(guid_str)) + RhinoUI.Panels.OpenPanel(guid) + print("[{}] Panel geoeffnet".format(mode.upper())) + except Exception as ex: + print("[{}] OpenPanel fehlgeschlagen: {}".format(mode.upper(), ex)) diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py new file mode 100644 index 0000000..c0a6795 --- /dev/null +++ b/rhino/rhinopanel.py @@ -0,0 +1,798 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +rhinopanel.py +Oeffnet das EBENEN-Panel (Zeichnungsebenen + globale Ebenen). +""" +import os +import sys +import json +import Rhino +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import panel_base +import layer_builder + +PANEL_GUID_STR = "3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" + +# Loop-Guard fuer Layer-Events (verhindert Endlos-Schleife bei eigenen Aenderungen) +def _is_processing(): + return bool(sc.sticky.get("ebenen_processing_layer", False)) + +def _set_processing(v): + sc.sticky["ebenen_processing_layer"] = bool(v) + + +def _hatch_pattern_names(doc): + """Liefert alle Hatch-Pattern-Namen aus doc.HatchPatterns als Liste.""" + out = [] + try: + for i in range(doc.HatchPatterns.Count): + try: + hp = doc.HatchPatterns[i] + if hp is None or hp.IsDeleted: continue + if hp.Name: out.append(hp.Name) + except Exception: + continue + except Exception: + pass + if not out: out = ["Solid"] + return out + + +class EbenenBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "ebenen") + + def _on_ready(self): + doc = Rhino.RhinoDoc.ActiveDoc + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + e_raw = doc.Strings.GetValue("dossier_ebenen") + if z_raw or e_raw: + try: + z = json.loads(z_raw) if z_raw else None + e = json.loads(e_raw) if e_raw else None + if z and e: + layer_builder.build_layers(doc, z, e) + layer_builder.cleanup_default_layers(doc) + self._ensure_active_sublayer() + self.send("STATE_SYNC", { + "zeichnungsebenen": z, + "ebenen": e, + "hatchPatterns": _hatch_pattern_names(doc), + }) + except Exception as ex: + print("[EBENEN] State-Sync:", ex) + else: + self.send("FIRST_RUN", {"hatchPatterns": _hatch_pattern_names(doc)}) + + def handle(self, data): + if not isinstance(data, dict): + return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): + p = {} + doc = Rhino.RhinoDoc.ActiveDoc + + if t == "READY": + self._on_ready() + elif t == "APPLY": + self._apply(p.get("zeichnungsebenen") or [], p.get("ebenen") or []) + elif t == "LAYER_STYLE": + layer_builder.update_layer_style(doc, p["code"], p.get("color"), p.get("lw")) + if p.get("color") is not None: + self._update_ebene_field(p["code"], "color", p["color"]) + if p.get("lw") is not None: + self._update_ebene_field(p["code"], "lw", p["lw"]) + elif t == "SET_ACTIVE": + self._set_active_zeichnungsebene(p) + elif t == "SET_ACTIVE_LAYER": + code = p.get("code", "") + if code: + doc.Strings.SetString("dossier_active_code", code) + self._set_active_sublayer(code) + elif t == "DELETE_EBENE": + layer_builder.delete_ebene(doc, p.get("code", ""), p.get("moveTo")) + self._remove_ebene_from_state(p.get("code", "")) + elif t == "MOVE_SELECTION_TO_LAYER": + self._move_selection_to_layer(p.get("code", "")) + elif t == "SET_VISIBILITY": + self._apply_visibility(p) + # --- Ebenen-Kombinationen (geteilter Store mit Ausschnitten) ------- + elif t == "GET_COMBINATION": + self._send_combination() + elif t == "APPLY_COMBINATION": + self._apply_combination(p) + self._send_combination() + elif t == "SAVE_PRESET": + self._save_preset(p.get("name") or "", p.get("layers") or []) + self._send_combination() + elif t == "SAVE_CURRENT_AS_PRESET": + self._save_current_as_preset(p.get("name") or "") + self._send_combination() + elif t == "DELETE_PRESET": + self._delete_preset(p.get("name") or "") + self._send_combination() + + # ---- Helpers ---- + + def _apply(self, zeichnungsebenen, ebenen): + print("[EBENEN] _apply START z={} e={}".format( + len(zeichnungsebenen) if zeichnungsebenen else 0, + len(ebenen) if ebenen else 0)) + doc = Rhino.RhinoDoc.ActiveDoc + + # Vor dem Schreiben: alten Fill-Stand snapshotten, damit wir hinterher + # entscheiden koennen ob refresh_layer_fills sich lohnt. + def _fill_signature(e_list): + out = {} + if not isinstance(e_list, list): return out + for e in e_list: + if not isinstance(e, dict): continue + f = e.get("fill") + if not isinstance(f, dict): continue + if f.get("pattern") in (None, "None"): continue + # lw kann None sein -> als Sentinel ein eindeutiger Wert + lw_raw = f.get("lw") + try: + lw_sig = round(float(lw_raw), 6) if lw_raw is not None else None + except Exception: + lw_sig = None + out[e.get("code")] = ( + f.get("pattern"), + f.get("source", "layer"), + (f.get("color") or "").lower(), + round(float(f.get("scale") or 1.0), 6), + round(float(f.get("rotation") or 0.0), 6), + lw_sig, + ) + return out + old_e_raw = doc.Strings.GetValue("dossier_ebenen") + old_sig = {} + if old_e_raw: + try: old_sig = _fill_signature(json.loads(old_e_raw)) + except Exception: old_sig = {} + new_sig = _fill_signature(ebenen) + fill_changed = (old_sig != new_sig) + + _set_processing(True) + try: + print("[EBENEN] _apply: build_layers ...") + layer_builder.build_layers(doc, zeichnungsebenen, ebenen) + print("[EBENEN] _apply: json.dumps ...") + # WICHTIG: ensure_ascii=False umgeht einen Bug in Rhinos eigener + # json/encoder.py die bei ASCII-escape s.decode('utf-8') aufruft + # und dabei mit 0xC4 (Umlaut) in den CP1252-Decoder lauft. + z_json = json.dumps(zeichnungsebenen, ensure_ascii=False) + e_json = json.dumps(ebenen, ensure_ascii=False) + print("[EBENEN] _apply: SetString ...") + doc.Strings.SetString("dossier_zeichnungsebenen", z_json) + doc.Strings.SetString("dossier_ebenen", e_json) + # Smart-Elemente (Waende) regenerieren — Geschoss-Hoehen/OKFF + # haben sich evtl. geaendert, gebundene Waende muessen neu + # extrudiert werden. Best-effort, faengt jeden Fehler ab. + try: + elem_bridge = sc.sticky.get("elemente_bridge") + if elem_bridge is not None: + elem_bridge._regenerate_all() + except Exception as _ex: + print("[EBENEN] elemente regen:", _ex) + n_with_fill = sum(1 for e in ebenen if isinstance(e, dict) + and isinstance(e.get("fill"), dict) + and e["fill"].get("pattern") not in (None, "None")) + print("[EBENEN] dossier_ebenen gespeichert: {} Ebenen, davon {} mit fill, JSON-len={}".format( + len(ebenen), n_with_fill, len(e_json))) + re_read = doc.Strings.GetValue("dossier_ebenen") + print("[EBENEN] dossier_ebenen verifiziert: len={}".format(len(re_read) if re_read else 0)) + print("[EBENEN] _apply: cleanup_default_layers ...") + layer_builder.cleanup_default_layers(doc) + print("[EBENEN] _apply: ensure_active_sublayer ...") + self._ensure_active_sublayer() + # Existierende 'Nach Ebene'-Hatches an neue Pattern/Skala/Drehung + # angleichen — ABER nur wenn die Fill-Signatur sich tatsaechlich + # geaendert hat (nicht bei reinen Name/Farb-Aenderungen, die das + # Settings-Dialog auch triggern koennte). + try: + import gestaltung + if fill_changed: + gestaltung.refresh_layer_fills(doc) + else: + print("[EBENEN] _apply: fill-Signatur unveraendert -> kein Hatch-Refresh") + # Plot-Color Repair laeuft immer (no-op falls schon synchron) + gestaltung.repair_plot_colors(doc) + except Exception as ex: + print("[EBENEN] gestaltung sync:", ex) + finally: + _set_processing(False) + print("[EBENEN] _apply: update_clipping ...") + self._update_clipping() + print("[EBENEN] _apply: send APPLY_OK") + self.send("APPLY_OK", {}) + print("[EBENEN] _apply: DONE") + + def _ensure_active_sublayer(self): + """Setzt den aktiven Rhino-Layer auf den DOSSIER-Sublayer (Fallback: erste Z + 20_WAENDE).""" + doc = Rhino.RhinoDoc.ActiveDoc + z_id = doc.Strings.GetValue("dossier_active_id") + code = doc.Strings.GetValue("dossier_active_code") or "20" + if not z_id: + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + if z_raw: + try: + z_list = json.loads(z_raw) + if z_list: + z_id = z_list[0].get("id", "") + if z_id: + doc.Strings.SetString("dossier_active_id", z_id) + except Exception: + pass + if z_id and code: + layer_builder.set_active_sublayer(doc, z_id, code) + + def _apply_visibility(self, p): + doc = Rhino.RhinoDoc.ActiveDoc + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + e_raw = doc.Strings.GetValue("dossier_ebenen") + if not z_raw or not e_raw: + return + try: + z_full = json.loads(z_raw) or [] + e_full = json.loads(e_raw) or [] + except Exception: + return + payload_z = p.get("zeichnungsebenen") or [] + payload_e = p.get("ebenen") or [] + z_state = {z["id"]: z for z in payload_z if isinstance(z, dict) and z.get("id")} + e_state = {e["code"]: e for e in payload_e if isinstance(e, dict) and e.get("code")} + merged_z = [] + for z in z_full: + if not isinstance(z, dict): continue + m = dict(z) + s = z_state.get(z.get("id")) + if s is not None: + m["visible"] = s.get("visible", True) + merged_z.append(m) + merged_e = [] + for e in e_full: + if not isinstance(e, dict): continue + m = dict(e) + s = e_state.get(e.get("code")) + if s is not None: + m["visible"] = s.get("visible", True) + m["locked"] = s.get("locked", False) + merged_e.append(m) + doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(merged_z, ensure_ascii=False)) + doc.Strings.SetString("dossier_ebenen", json.dumps(merged_e, ensure_ascii=False)) + active_z = p.get("activeZ") or {} + if not isinstance(active_z, dict): active_z = {} + layer_builder.apply_visibility( + doc, merged_z, merged_e, + active_z.get("id"), + p.get("activeCode"), + p.get("zMode") or "active", + p.get("eMode") or "all", + ) + + def _set_active_zeichnungsebene(self, z): + doc = Rhino.RhinoDoc.ActiveDoc + z_id = z.get("id", "") + doc.Strings.SetString("dossier_active_id", z_id) + # Clipping ggf. mitziehen + self._update_clipping(active_z=z) + # Elemente-Panel informieren: das aktive Geschoss hat gewechselt, + # neue Elemente sollen jetzt automatisch dort verlinkt werden. + try: + eb = sc.sticky.get("elemente_bridge") + if eb is not None: eb._send_state() + except Exception: pass + if not (z.get("isGeschoss") and z.get("okff") is not None): + return + okff = float(z["okff"]) + updated = 0 + for view in doc.Views: + try: + vp = view.ActiveViewport + cp = vp.ConstructionPlane() + plane = cp.Plane if hasattr(cp, "Plane") else cp + # Nur Views deren CPlane horizontal liegt (Normal in +/-Z) - + # also Top/Plan-Style. Right/Front/Perspective haben vertikale + # CPlanes; ein Z-Shift waere dort optisch verwirrend. + if abs(plane.Normal.Z) < 0.99: + continue + new_plane = Rhino.Geometry.Plane( + Rhino.Geometry.Point3d(plane.Origin.X, plane.Origin.Y, okff), + plane.XAxis, plane.YAxis, + ) + vp.SetConstructionPlane(new_plane) + view.Redraw() + updated += 1 + except Exception as 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)) + + def _update_clipping(self, active_z=None): + """Clipping-Plane folgt aktivem Geschoss — nur wenn dessen hasClipping=True.""" + doc = Rhino.RhinoDoc.ActiveDoc + if active_z is None: + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + active_id = doc.Strings.GetValue("dossier_active_id") + if z_raw and active_id: + try: + z_list = json.loads(z_raw) + active_z = next((z for z in z_list if z.get("id") == active_id), None) + except Exception: + active_z = None + enabled = bool(active_z and active_z.get("hasClipping")) + _set_processing(True) + try: + layer_builder.update_clipping_plane(doc, active_z, enabled) + finally: + _set_processing(False) + + def _move_selection_to_layer(self, code): + if not code: + return + doc = Rhino.RhinoDoc.ActiveDoc + z_id = doc.Strings.GetValue("dossier_active_id") + if not z_id: + print("[EBENEN] Keine aktive Zeichnungsebene") + return + parent_idx = layer_builder._find_top_by_id(doc, z_id) + if parent_idx < 0: + print("[EBENEN] Parent fuer aktive Zeichnungsebene nicht gefunden") + return + parent_id = doc.Layers[parent_idx].Id + sub_idx = layer_builder._find_sublayer_by_code(doc, parent_id, code) + if sub_idx < 0: + print("[EBENEN] Sublayer {} unter {} nicht gefunden".format(code, doc.Layers[parent_idx].Name)) + return + objs = list(doc.Objects.GetSelectedObjects(False, False)) + moved = 0 + for obj in objs: + attrs = obj.Attributes.Duplicate() + attrs.LayerIndex = sub_idx + if doc.Objects.ModifyAttributes(obj, attrs, True): + moved += 1 + doc.Views.Redraw() + print("[EBENEN] {} Objekt(e) auf {} verschoben".format(moved, doc.Layers[sub_idx].FullPath)) + + def _set_active_sublayer(self, code): + if not code: + return + doc = Rhino.RhinoDoc.ActiveDoc + z_id = doc.Strings.GetValue("dossier_active_id") + if not z_id: + # Fallback: erste Zeichnungsebene aus persistiertem State + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + if z_raw: + try: + z_list = json.loads(z_raw) + if z_list: + z_id = z_list[0].get("id", "") + if z_id: + doc.Strings.SetString("dossier_active_id", z_id) + except Exception: + pass + if z_id: + layer_builder.set_active_sublayer(doc, z_id, code) + else: + print("[EBENEN] Aktive Zeichnungsebene unbekannt — Layer wird nicht gesetzt") + + def _remove_ebene_from_state(self, code): + doc = Rhino.RhinoDoc.ActiveDoc + raw = doc.Strings.GetValue("dossier_ebenen") + if not raw: + return + try: + ebenen = [e for e in json.loads(raw) if e.get("code") != code] + doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False)) + except Exception as ex: + print("[EBENEN] remove:", ex) + + def _update_ebene_field(self, code, field, value): + doc = Rhino.RhinoDoc.ActiveDoc + raw = doc.Strings.GetValue("dossier_ebenen") + if not raw: + return + try: + ebenen = json.loads(raw) + for e in ebenen: + if e.get("code") == code: + e[field] = value + break + doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False)) + except Exception as ex: + print("[EBENEN] update:", ex) + + # ---- Ebenen-Kombinationen / Presets (geteilt mit AUSSCHNITTE) -------- + + _PRESETS_KEY = "dossier_layer_presets" + + def _load_presets(self, doc): + raw = doc.Strings.GetValue(self._PRESETS_KEY) + if not raw: return [] + try: + data = json.loads(raw) + return data if isinstance(data, list) else [] + except Exception: + return [] + + def _store_presets(self, doc, presets): + try: + doc.Strings.SetString(self._PRESETS_KEY, + json.dumps(presets, ensure_ascii=False)) + except Exception as ex: + print("[EBENEN] _store_presets:", ex) + + def _send_combination(self): + """Schickt aktuelles Layer-State + alle Presets ans Frontend.""" + doc = Rhino.RhinoDoc.ActiveDoc + layers_out = [] + try: + for layer in doc.Layers: + if layer is None or layer.IsDeleted: continue + lid = str(layer.Id) + try: + fp = layer.FullPath or layer.Name + except Exception: + fp = layer.Name or "" + try: + col = "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B) + except Exception: + col = "#888888" + layers_out.append({ + "id": lid, + "name": layer.Name, + "fullPath": fp, + "color": col, + "visible": bool(layer.IsVisible), + "locked": bool(layer.IsLocked), + }) + layers_out.sort(key=lambda x: x["fullPath"]) + except Exception as ex: + print("[EBENEN] _send_combination layers:", ex) + try: + presets = self._load_presets(doc) + except Exception: + presets = [] + self.send("COMBINATION_DATA", { + "layers": layers_out, + "presets": presets, + }) + + def _apply_combination(self, payload): + """Wendet Preset an. payload kann sein: + - Liste [{id, visible, locked}, ...] (alt / AUSSCHNITTE-Dialog) + - Dict { layers, dossierEbenen?, dossierZeichnungsebenen? } (neu) + + Eye-State-Pfad (bevorzugt): aktualisiert dossier_ebenen und + dossier_zeichnungsebenen direkt, pusht STATE_SYNC. React triggert + dann SET_VISIBILITY und apply_visibility setzt doc.Layer korrekt + unter Beruecksichtigung von z_mode/e_mode. + + Layer-ID-Pfad (Fallback): setzt doc.Layer.IsVisible direkt. + """ + doc = Rhino.RhinoDoc.ActiveDoc + + # Payload normalisieren + if isinstance(payload, dict): + layer_states = payload.get("layers") or [] + pe_states = payload.get("dossierEbenen") + pz_states = payload.get("dossierZeichnungsebenen") + else: + layer_states = payload or [] + pe_states = None + pz_states = None + + # --- Eye-State-Pfad (wenn vorhanden) --- + if pe_states is not None or pz_states is not None: + try: + e_raw = doc.Strings.GetValue("dossier_ebenen") or "[]" + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]" + e_list = json.loads(e_raw) or [] + z_list = json.loads(z_raw) or [] + if pe_states is not None: + by_code = {x.get("code"): x for x in pe_states if isinstance(x, dict) and x.get("code")} + for e in e_list: + if not isinstance(e, dict): continue + s = by_code.get(e.get("code")) + if s is None: continue + e["visible"] = bool(s.get("visible", True)) + e["locked"] = bool(s.get("locked", False)) + if pz_states is not None: + by_id = {x.get("id"): x for x in pz_states if isinstance(x, dict) and x.get("id")} + for z in z_list: + if not isinstance(z, dict): continue + s = by_id.get(z.get("id")) + if s is None: continue + z["visible"] = bool(s.get("visible", True)) + doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False)) + doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False)) + # STATE_SYNC pushen — React's visibilityKey aendert sich, + # applyVisibility fires, backend apply_visibility setzt doc.Layer + # state korrekt unter z_mode/e_mode-Beachtung. + self.send("STATE_SYNC", { + "zeichnungsebenen": z_list, + "ebenen": e_list, + }) + try: doc.Views.Redraw() + except Exception: pass + print("[EBENEN] Eye-State-Preset angewandt: {} Ebenen, {} Zeichnungsebenen".format( + len(pe_states or []), len(pz_states or []))) + return + except Exception as ex: + print("[EBENEN] _apply_combination eye-state:", ex) + # Fall through zum Layer-ID-Pfad als Fallback + + # --- Layer-ID-Pfad (alt / AUSSCHNITTE) --- + by_id = {} + for layer in doc.Layers: + if not layer.IsDeleted: + by_id[str(layer.Id)] = layer + n = 0 + # Erst: doc.Layer Visibility setzen + _set_processing(True) + try: + for ls in (layer_states or []): + layer = by_id.get(ls.get("id")) + if layer is None: continue + try: + want_vis = bool(ls.get("visible", True)) + want_lck = bool(ls.get("locked", False)) + if layer.IsVisible != want_vis: + layer.IsVisible = want_vis + if layer.IsLocked != want_lck: + layer.IsLocked = want_lck + n += 1 + except Exception: pass + finally: + _set_processing(False) + # Dann: dossier_ebenen/dossier_zeichnungsebenen Eye-State synchronisieren. + # Map: doc.Layer.Id -> {visible, locked} + state_by_id = {ls.get("id"): ls for ls in (layer_states or []) if ls.get("id")} + try: + e_raw = doc.Strings.GetValue("dossier_ebenen") + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + ebenen_list = json.loads(e_raw) if e_raw else [] + z_list = json.loads(z_raw) if z_raw else [] + # Sublayer -> dossier_code mapping via Rhino-Layer UserString + code_by_layer_id = {} + zid_by_layer_id = {} + for layer in doc.Layers: + if layer is None or layer.IsDeleted: continue + c = layer.GetUserString("dossier_code") + i = layer.GetUserString("dossier_id") + if c: code_by_layer_id[str(layer.Id)] = c + if i: zid_by_layer_id[str(layer.Id)] = i + # Pro Dossier-Ebene: wenn mind. ein matchender Sublayer im preset war, + # sync visible/locked. + updated_e = False + for e in ebenen_list: + if not isinstance(e, dict): continue + code = e.get("code") + if not code: continue + # Suche eine Layer-Id mit diesem code, deren state im preset ist + for lid, c in code_by_layer_id.items(): + if c != code: continue + s = state_by_id.get(lid) + if s is None: continue + new_vis = bool(s.get("visible", True)) + new_lck = bool(s.get("locked", False)) + if e.get("visible", True) != new_vis: + e["visible"] = new_vis + updated_e = True + if (e.get("locked", False)) != new_lck: + e["locked"] = new_lck + updated_e = True + break + updated_z = False + for z in z_list: + if not isinstance(z, dict): continue + zid = z.get("id") + if not zid: continue + for lid, z_uid in zid_by_layer_id.items(): + if z_uid != zid: continue + s = state_by_id.get(lid) + if s is None: continue + new_vis = bool(s.get("visible", True)) + if z.get("visible", True) != new_vis: + z["visible"] = new_vis + updated_z = True + break + if updated_e: + doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen_list, ensure_ascii=False)) + if updated_z: + doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False)) + # STATE_SYNC ans React-Panel pushen damit Eye-Icons matchen + if updated_e or updated_z: + try: + self.send("STATE_SYNC", { + "zeichnungsebenen": z_list, + "ebenen": ebenen_list, + }) + except Exception as ex: + print("[EBENEN] STATE_SYNC push:", ex) + except Exception as ex: + print("[EBENEN] _apply_combination sync:", ex) + try: doc.Views.Redraw() + except Exception: pass + print("[EBENEN] Kombination angewandt: {} Layer".format(n)) + + def _save_preset(self, name, layers): + name = (name or "").strip() + if not name: return + doc = Rhino.RhinoDoc.ActiveDoc + presets = self._load_presets(doc) + clean = [] + for ls in (layers or []): + lid = ls.get("id") + if not lid: continue + clean.append({ + "id": lid, + "visible": bool(ls.get("visible", True)), + "locked": bool(ls.get("locked", False)), + }) + existing = next((p for p in presets if p.get("name") == name), None) + if existing is not None: + existing["layers"] = clean + else: + presets.append({"name": name, "layers": clean}) + self._store_presets(doc, presets) + print("[EBENEN] Kombination '{}' gespeichert ({} Layer)".format(name, len(clean))) + + def _save_current_as_preset(self, name): + """Speichert die aktuellen Eye-States (dossier_ebenen + dossier_zeichnungs- + ebenen) als Preset — NICHT die berechneten doc.Layer.IsVisible-Werte. + Sonst wuerde der z_mode/e_mode-Override (z.B. 'active' nur 1 Layer + sichtbar) ins Preset einbacken und beim Apply nicht wieder restorbar + sein. + + layers (doc.Layer-Liste) wird parallel mitgespeichert fuer Kompat + mit AUSSCHNITTE (das vom doc.Layer-State liest).""" + name = (name or "").strip() + if not name: return + doc = Rhino.RhinoDoc.ActiveDoc + # 1) doc.Layer state (Kompat mit AUSSCHNITTE) + layers = [] + try: + for layer in doc.Layers: + if layer is None or layer.IsDeleted: continue + layers.append({ + "id": str(layer.Id), + "visible": bool(layer.IsVisible), + "locked": bool(layer.IsLocked), + }) + except Exception as ex: + print("[EBENEN] _save_current_as_preset enum:", ex) + # 2) Eye-States aus dossier_ebenen / dossier_zeichnungsebenen + pe_state = [] + pz_state = [] + try: + e_raw = doc.Strings.GetValue("dossier_ebenen") + if e_raw: + for e in (json.loads(e_raw) or []): + if isinstance(e, dict) and e.get("code"): + pe_state.append({ + "code": e["code"], + "visible": bool(e.get("visible", True)), + "locked": bool(e.get("locked", False)), + }) + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + if z_raw: + for z in (json.loads(z_raw) or []): + if isinstance(z, dict) and z.get("id"): + pz_state.append({ + "id": z["id"], + "visible": bool(z.get("visible", True)), + }) + except Exception as ex: + print("[EBENEN] _save_current_as_preset eye-states:", ex) + presets = self._load_presets(doc) + new_data = { + "name": name, + "layers": layers, + "dossierEbenen": pe_state, + "dossierZeichnungsebenen": pz_state, + } + existing = next((p for p in presets if p.get("name") == name), None) + if existing is not None: + existing.update(new_data) + else: + presets.append(new_data) + self._store_presets(doc, presets) + print("[EBENEN] '{}' gespeichert: {} Layer + {} Ebenen Eye-State".format( + name, len(layers), len(pe_state))) + + def _delete_preset(self, name): + name = (name or "").strip() + if not name: return + doc = Rhino.RhinoDoc.ActiveDoc + presets = [p for p in self._load_presets(doc) if p.get("name") != name] + self._store_presets(doc, presets) + print("[EBENEN] Kombination '{}' geloescht".format(name)) + + +def _ebenen_bridge_factory(): + bridge = EbenenBridge() + _install_layer_listener(bridge) + return bridge + + +def _install_layer_listener(bridge): + """Reagiert auf externe Aenderungen in Rhinos Layer-Tabelle (Rename, Delete).""" + if sc.sticky.get("ebenen_layer_listener"): + sc.sticky["ebenen_bridge_ref"] = bridge + return + sc.sticky["ebenen_bridge_ref"] = bridge + + def on_layer_event(sender, args): + if _is_processing(): + return + try: + doc = args.Document + evt = args.EventType + # Nur Modify-Events interessieren uns (Rename, Color etc.) + if evt != Rhino.DocObjects.Tables.LayerTableEventType.Modified: + return + idx = args.LayerIndex + if idx < 0 or idx >= doc.Layers.Count: + return + layer = doc.Layers[idx] + dossier_id = layer.GetUserString("dossier_id") + dossier_code = layer.GetUserString("dossier_code") + if not (dossier_id or dossier_code): + return + updated = False + if dossier_id: + raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + if raw: + try: + z_list = json.loads(raw) + for z in z_list: + if z.get("id") == dossier_id and z.get("name") != layer.Name: + z["name"] = layer.Name + updated = True + break + if updated: + doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(z_list, ensure_ascii=False)) + except Exception: + pass + elif dossier_code: + raw = doc.Strings.GetValue("dossier_ebenen") + if raw: + try: + e_list = json.loads(raw) + # Layer-Name ist "CC_NAME" — wir extrahieren NAME + if "_" in layer.Name: + new_name = layer.Name.split("_", 1)[1] + for e in e_list: + if e.get("code") == dossier_code and e.get("name") != new_name: + e["name"] = new_name + updated = True + break + if updated: + doc.Strings.SetString("dossier_ebenen", json.dumps(e_list, ensure_ascii=False)) + except Exception: + pass + if updated: + b = sc.sticky.get("ebenen_bridge_ref") + if b is not None: + try: + b._on_ready() # sendet aktualisiertes STATE_SYNC + except Exception: + pass + except Exception as ex: + print("[EBENEN] Layer-Event:", ex) + + Rhino.RhinoDoc.LayerTableEvent += on_layer_event + sc.sticky["ebenen_layer_listener"] = True + print("[EBENEN] Layer-Listener aktiv") + + +panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory, + icon_spec=("E", "#3a6fa8")) diff --git a/rhino/startup.py b/rhino/startup.py new file mode 100644 index 0000000..a7a32bc --- /dev/null +++ b/rhino/startup.py @@ -0,0 +1,136 @@ +# ! 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 + +_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") diff --git a/rhino/werkzeuge.py b/rhino/werkzeuge.py new file mode 100644 index 0000000..7957b7d --- /dev/null +++ b/rhino/werkzeuge.py @@ -0,0 +1,58 @@ +# ! python3 +# -*- coding: utf-8 -*- +""" +werkzeuge.py +WERKZEUGE-Panel: Architektur-orientierte Toolbar als React-WebView. +Feuert Rhino-Befehle via RunScript bei Button-Klick. +""" +import os +import sys +import Rhino +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +import panel_base + +PANEL_GUID_STR = "6d9f5040-7e1f-4f2b-c4d5-f6071829304a" + + +class WerkzeugeBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "werkzeuge") + + def _on_ready(self): + # Keine initialen Daten noetig — Toolbar ist statisch + pass + + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + p = data.get("payload") or {} + if not isinstance(p, dict): p = {} + if t == "READY": + self._on_ready() + elif t == "RUN": + cmd = p.get("cmd") + if cmd and isinstance(cmd, str): + # Whitelist: alles muss mit "_" beginnen (Rhino-Befehl) und + # darf keine Zeilenumbrueche oder Semikolons enthalten. + cmd = cmd.strip() + if cmd.startswith("_") and "\n" not in cmd and ";" not in cmd: + try: + Rhino.RhinoApp.RunScript(cmd, False) + print("[WERKZEUGE] {}".format(cmd)) + except Exception as ex: + print("[WERKZEUGE] RunScript-Fehler:", ex) + else: + print("[WERKZEUGE] Befehl ignoriert (kein '_' Praefix oder unsicher):", cmd) + + +def _bridge_factory(): + return WerkzeugeBridge() + + +panel_base.register_and_open("werkzeuge", "WERKZEUGE", PANEL_GUID_STR, _bridge_factory, + icon_spec=("W", "#3a6fa8")) diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..dddf528 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,273 @@ +import { useState, useEffect, useMemo, useRef } from 'react' +import GeschossManager from './components/GeschossManager' +import EbenenManager from './components/EbenenManager' +import AusschnittLayerDialog from './components/AusschnittLayerDialog' +import { + applyAll, setActiveZeichnungsebene, setActiveEbene, + onMessage, notifyReady, applyVisibility, + getCombination, applyCombination, + saveCurrentAsCombination, deleteCombinationPreset, + saveCombinationPreset, +} from './lib/rhinoBridge' + +export function recalcOkff(list) { + let acc = 0 + return list.map(z => { + if (z.isGeschoss) { + const next = { ...z, okff: parseFloat(acc.toFixed(3)) } + acc += (z.hoehe ?? 3.0) + return next + } + return { ...z, okff: undefined } + }) +} + +const INITIAL_ZEICHNUNGSEBENEN = recalcOkff([ + { id: 'eg', name: 'EG', isGeschoss: true, hoehe: 3.50, schnitthoehe: 1.00, visible: true }, + { id: '1og', name: '1OG', isGeschoss: true, hoehe: 3.00, schnitthoehe: 1.00, visible: true }, + { id: '2og', name: '2OG', isGeschoss: true, hoehe: 3.00, schnitthoehe: 1.00, visible: true }, +]) + +const INITIAL_EBENEN = [ + { code: '00', name: 'RASTER', color: '#484850', lw: 0.13, visible: true, locked: false }, + { code: '01', name: 'VERMESSUNG', color: '#707078', lw: 0.18, visible: true, locked: false }, + { code: '10', name: 'SITUATION', color: '#909090', lw: 0.18, visible: true, locked: false }, + { code: '11', name: 'STRASSE', color: '#a89070', lw: 0.18, visible: true, locked: false }, + { code: '12', name: 'GEBÄUDE', color: '#888888', lw: 0.25, visible: true, locked: false }, + { code: '13', name: 'BÄUME', color: '#50a050', lw: 0.13, visible: true, locked: false }, + { code: '14', name: 'HÖHENLINIEN', color: '#909050', lw: 0.18, visible: true, locked: false }, + { code: '20', name: 'WÄNDE', color: '#0a0a0a', lw: 0.50, visible: true, locked: false }, + { code: '21', name: 'TÜREN_FENSTER', color: '#5080c8', lw: 0.25, visible: true, locked: false }, + { code: '22', name: 'MÖBEL', color: '#909090', lw: 0.13, visible: true, locked: false }, + { code: '25', name: 'STÜTZEN', color: '#c87050', lw: 0.50, visible: true, locked: false }, + { code: '30', name: 'DECKEN', color: '#605850', lw: 0.35, visible: true, locked: false }, + { code: '31', name: 'DÄCHER', color: '#7a4a3a', lw: 0.35, visible: true, locked: false }, + { code: '35', name: 'TRÄGER', color: '#a87858', lw: 0.50, visible: true, locked: false }, + { code: '50', name: 'TEXT', color: '#d0d0d0', lw: 0.13, visible: true, locked: false }, + { code: '60', name: 'PLANGRAFIK', color: '#c0a040', lw: 0.13, visible: true, locked: false }, + { code: '90', name: 'REFERENZEN', color: '#585860', lw: 0.13, visible: true, locked: false }, + { code: '99', name: 'KONSTRUKTION', color: '#404048', lw: 0.13, visible: true, locked: false }, +] + +export default function App() { + const [zeichnungsebenen, setZeichnungsebenen] = useState(INITIAL_ZEICHNUNGSEBENEN) + const [ebenen, setEbenen] = useState(INITIAL_EBENEN) + const [activeId, setActiveId] = useState('eg') + const [activeCode, setActiveCode] = useState('20') + const [appliedZ, setAppliedZ] = useState(INITIAL_ZEICHNUNGSEBENEN) + const [appliedE, setAppliedE] = useState(INITIAL_EBENEN) + const [zMode, setZMode] = useState('active') + const [eMode, setEMode] = useState('all') + const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60']) + // Ebenenkombinationen (geteilter Store mit Ausschnitten) + const [combinations, setCombinations] = useState([]) // Liste { name, layers } + const [activeCombName, setActiveCombName] = useState(null) // null = "Eigene" + // Dialog fuer "alle bearbeiten" (Pencil-Button) + const [combDialog, setCombDialog] = useState(null) // { layers, presets } oder null + const wantCombDialogRef = useRef(false) + + useEffect(() => { + onMessage('STATE_SYNC', ({ zeichnungsebenen: z, ebenen: e, hatchPatterns: hp }) => { + if (z) { + const r = recalcOkff(z); setZeichnungsebenen(r); setAppliedZ(r) + const active = r.find(zz => zz.id === activeId) || r[0] + if (active) { + setActiveZeichnungsebene(active) + // Auch den Sublayer-Code aktiv setzen, damit Rhino's Current-Layer + // beim Panel-Start sofort auf der Wahl im Panel landet (sonst bleibt + // "Default" und neue Objekte landen dort). + if (activeCode) setActiveEbene(activeCode) + } + } + if (e) { setEbenen(e); setAppliedE(e) } + if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp) + }) + onMessage('COMBINATION_DATA', ({ layers, presets }) => { + setCombinations(presets || []) + if (wantCombDialogRef.current) { + wantCombDialogRef.current = false + setCombDialog({ layers: layers || [], presets: presets || [] }) + } else if (combDialog) { + // Dialog ist offen — Layer-Liste live aktualisieren (z.B. nach Preset-Save) + setCombDialog(d => d ? { ...d, layers: layers || d.layers, presets: presets || [] } : d) + } + }) + onMessage('FIRST_RUN', () => { + applyAll(INITIAL_ZEICHNUNGSEBENEN, INITIAL_EBENEN) + setAppliedZ(INITIAL_ZEICHNUNGSEBENEN) + setAppliedE(INITIAL_EBENEN) + const active = INITIAL_ZEICHNUNGSEBENEN.find(zz => zz.id === activeId) || INITIAL_ZEICHNUNGSEBENEN[0] + if (active) { + setActiveZeichnungsebene(active) + if (activeCode) setActiveEbene(activeCode) + } + }) + notifyReady() + // Initial Liste der Kombinationen holen + setTimeout(() => getCombination(), 200) + + // Native Browser-Context-Menu global unterdruecken + const blockContext = (ev) => ev.preventDefault() + document.addEventListener('contextmenu', blockContext) + return () => document.removeEventListener('contextmenu', blockContext) + }, []) + + // Sichtbarkeit live anwenden — bei relevanten Aenderungen + const visibilityKey = useMemo(() => ( + activeId + '|' + activeCode + '|' + zMode + '|' + eMode + '|' + + zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}`).join(',') + '|' + + ebenen.map(e => `${e.code}:${e.visible !== false ? 1 : 0}:${e.locked === true ? 1 : 0}`).join(',') + ), [activeId, activeCode, zMode, eMode, zeichnungsebenen, ebenen]) + + useEffect(() => { + const activeZ = zeichnungsebenen.find(z => z.id === activeId) + if (activeZ) applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, zMode, eMode) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visibilityKey]) + + // Auto-Apply bei strukturellen Aenderungen (add/remove/rename) UND + // wenn die Ebenen-Settings (z.B. fill) sich aendern — Python braucht den + // neuen Stand in doc.Strings damit Auto-Fill und 'Nach Ebene' korrekt lesen. + // Kanonische Signatur: leere/None-Fills sind alle aequivalent — sonst loest + // schon das blosse Oeffnen+Schliessen des Settings-Dialogs ein applyAll aus + // (Dialog initialisiert fill mit Default-Werten). + const fillSig = (e) => { + const f = e.fill + if (!f || !f.pattern || f.pattern === 'None') return '' + return [f.pattern, f.source || 'layer', f.color || '', f.scale ?? 1, f.rotation ?? 0].join('|') + } + const structureKey = useMemo(() => ( + zeichnungsebenen.map(z => `${z.id}:${z.name}:${z.isGeschoss ? 1 : 0}`).join(',') + '|' + + ebenen.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',') + ), [zeichnungsebenen, ebenen]) + + const appliedStructureKey = useMemo(() => ( + appliedZ.map(z => `${z.id}:${z.name}:${z.isGeschoss ? 1 : 0}`).join(',') + '|' + + appliedE.map(e => `${e.code}:${e.name}:${fillSig(e)}`).join(',') + ), [appliedZ, appliedE]) + + useEffect(() => { + if (structureKey === appliedStructureKey) return + const t = setTimeout(() => { + applyAll(zeichnungsebenen, ebenen) + setAppliedZ(zeichnungsebenen) + setAppliedE(ebenen) + }, 200) + return () => clearTimeout(t) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [structureKey, appliedStructureKey]) + + // --- Ebenen-Kombinationen ---------------------------------------------- + const handlePickCombination = (name) => { + if (!name) { setActiveCombName(null); return } + const preset = combinations.find(p => p.name === name) + if (!preset) return + // Eye-State bevorzugen wenn im Preset vorhanden (= verlustfreie Wiederherstellung, + // beruecksichtigt z_mode/e_mode); fallback auf doc.Layer-Liste fuer alte Presets. + applyCombination({ + layers: preset.layers || [], + dossierEbenen: preset.dossierEbenen, + dossierZeichnungsebenen: preset.dossierZeichnungsebenen, + }) + setActiveCombName(name) + } + const handleSaveCurrentCombination = () => { + const suggested = activeCombName || `Kombi ${combinations.length + 1}` + const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim() + if (!name) return + if (combinations.some(p => p.name === name) && + !window.confirm(`"${name}" ueberschreiben?`)) return + saveCurrentAsCombination(name) + setActiveCombName(name) + } + const handleDeleteCombination = (name) => { + if (!name) return + if (!window.confirm(`Ebenenkombination "${name}" löschen?`)) return + deleteCombinationPreset(name) + if (activeCombName === name) setActiveCombName(null) + } + const handleOpenCombDialog = () => { + wantCombDialogRef.current = true + getCombination() + } + // Wenn der User Sichtbarkeit/Lock manuell aendert -> "Eigene". + // Wird direkt von EbenenManager aufgerufen, kein Effect-Race. + const handleUserVisibilityChange = () => { + if (activeCombName !== null) setActiveCombName(null) + } + + const handleActiveChange = (id) => { + setActiveId(id) + const z = zeichnungsebenen.find(x => x.id === id) + if (z) { + setActiveZeichnungsebene(z) + if (activeCode) setActiveEbene(activeCode) + } + } + + return ( +
+
+ setZeichnungsebenen(recalcOkff(updated))} + recalcOkff={recalcOkff} + mode={zMode} + onModeChange={setZMode} + /> + { setActiveCode(code); setActiveEbene(code) }} + onChange={setEbenen} + mode={eMode} + onModeChange={setEMode} + hatchPatterns={hatchPatterns} + combinations={combinations} + activeCombName={activeCombName} + onPickCombination={handlePickCombination} + onSaveCurrentCombination={handleSaveCurrentCombination} + onDeleteCombination={handleDeleteCombination} + onEditCombinations={handleOpenCombDialog} + onUserVisibilityChange={handleUserVisibilityChange} + /> +
+ + {combDialog && ( + setCombDialog(null)} + onSave={(layers) => { + applyCombination(layers) + setActiveCombName(null) + setCombDialog(null) + }} + onSavePreset={(name, layers) => { + saveCombinationPreset(name, layers) + setCombDialog(d => d ? { + ...d, + presets: [...d.presets.filter(p => p.name !== name), { name, layers }], + } : d) + }} + onDeletePreset={(name) => { + deleteCombinationPreset(name) + setCombDialog(d => d ? { + ...d, + presets: d.presets.filter(p => p.name !== name), + } : d) + if (activeCombName === name) setActiveCombName(null) + }} + /> + )} +
+ ) +} diff --git a/src/AusschnitteApp.jsx b/src/AusschnitteApp.jsx new file mode 100644 index 0000000..0f51ec9 --- /dev/null +++ b/src/AusschnitteApp.jsx @@ -0,0 +1,515 @@ +import { useState, useEffect, useMemo } from 'react' +import Icon from './components/Icon' +import ContextMenu from './components/ContextMenu' +import AusschnittLayerDialog from './components/AusschnittLayerDialog' +import { + onMessage, notifyReady, + listAusschnitte, saveAusschnitt, updateAusschnitt, + restoreAusschnitt, applyAusschnittToDetail, + renameAusschnitt, deleteAusschnitt, + setAusschnittFolder, setAusschnittScale, + duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder, + getAusschnittLayers, updateAusschnittLayers, + saveLayerPreset, deleteLayerPreset, +} from './lib/rhinoBridge' + +function EditableInline({ value, onCommit, autoEdit, style, fontSize }) { + const [editing, setEditing] = useState(autoEdit || false) + const [val, setVal] = useState(value) + useEffect(() => { setVal(value) }, [value]) + useEffect(() => { if (autoEdit) setEditing(true) }, [autoEdit]) + + const commit = () => { + const trimmed = (val ?? '').trim() + if (trimmed && trimmed !== value) onCommit(trimmed) + else setVal(value) + setEditing(false) + } + if (editing) { + return ( + setVal(ev.target.value)} + onBlur={commit} + onKeyDown={(ev) => { + if (ev.key === 'Enter') commit() + if (ev.key === 'Escape') { setVal(value); setEditing(false) } + }} + onClick={(ev) => ev.stopPropagation()} + style={{ ...style, fontSize, padding: '2px 6px' }} + /> + ) + } + return ( + { ev.stopPropagation(); setVal(value); setEditing(true) }} + style={{ ...style, fontSize, cursor: 'text' }} + >{value} + ) +} + +function ScaleCell({ snap, onChange }) { + const [editing, setEditing] = useState(false) + const [val, setVal] = useState(snap.scale || '') + useEffect(() => { setVal(snap.scale || '') }, [snap.scale]) + + const commit = () => { + onChange(snap.id, val.trim()) + setEditing(false) + } + if (editing) { + return ( + setVal(ev.target.value)} + onBlur={commit} + onKeyDown={(ev) => { + if (ev.key === 'Enter') commit() + if (ev.key === 'Escape') { setVal(snap.scale || ''); setEditing(false) } + }} + onClick={(ev) => ev.stopPropagation()} + placeholder="1:50" + style={{ width: 64, fontSize: 10, padding: '2px 6px', textAlign: 'right' }} + /> + ) + } + return ( + { ev.stopPropagation(); setEditing(true) }} + className={snap.scale ? 'chip chip-accent' : 'chip'} + style={{ + fontSize: 9, cursor: 'text', + fontFamily: 'var(--font-mono)', + flexShrink: 0, + }} + title={snap.scale ? `Maßstab ${snap.scale} — wird auf Detail angewendet` : 'Doppelklick um Maßstab einzutragen (z.B. 1:50)'} + >{snap.scale || '—:—'} + ) +} + +function OrientationBadge({ orientation }) { + const variant = orientation === 'perspective' ? { + icon: 'view_in_ar', color: 'var(--accent)', + title: 'Perspektive', + } : orientation === 'horizontal' ? { + icon: 'align_horizontal_center', color: 'var(--active)', + title: 'Horizontaler Schnitt (Grundriss)', + } : { + icon: 'align_vertical_center', color: 'var(--warn)', + title: 'Vertikaler Schnitt (Schnitt / Ansicht)', + } + return ( + + + + ) +} + +function AusschnittCard({ snap, onClick, onContextMenu, onMenuClick, onRename, onScaleChange, onDragStart, onDragEnd, dragging }) { + return ( +
{ ev.currentTarget.style.background = 'var(--bg-item-hover)' }} + onMouseLeave={(ev) => { ev.currentTarget.style.background = 'var(--bg-input)' }} + > + + onRename(snap.id, n)} + fontSize={11} + style={{ + flex: 1, minWidth: 0, + color: 'var(--text-primary)', fontWeight: 500, + overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', + }} + /> + + +
+ ) +} + +function FolderCard({ + name, count, collapsed, onToggle, onContextMenu, onMenuClick, + onDragOver, onDragLeave, onDrop, dragOver, children, +}) { + return ( +
+
+ + + + + {name} + {count} + +
+ {!collapsed && ( +
+ {children || ( +
+ Leer — Ausschnitte hier ablegen. +
+ )} +
+ )} +
+ ) +} + +function RootDropZone({ children, onDragOver, onDragLeave, onDrop, dragOver, empty }) { + return ( +
+ {children} +
+ ) +} + +export default function AusschnitteApp() { + const [snaps, setSnaps] = useState([]) + const [extraFolders, setExtraFolders] = useState([]) + const [presets, setPresets] = useState([]) + const [newName, setNewName] = useState('') + const [ctxMenu, setCtxMenu] = useState(null) + const [collapsed, setCollapsed] = useState({}) + const [draggingId, setDraggingId] = useState(null) + const [dragTarget, setDragTarget] = useState(null) + const [layerDialog, setLayerDialog] = useState(null) + + useEffect(() => { + onMessage('LIST', ({ snapshots, folders, presets }) => { + setSnaps(snapshots || []) + setExtraFolders(folders || []) + setPresets(presets || []) + }) + onMessage('LAYERS_DATA', ({ id, name, layers, presets }) => { + setLayerDialog({ id, name, layers: layers || [], presets: presets || [] }) + }) + notifyReady() + const blockContext = (ev) => ev.preventDefault() + document.addEventListener('contextmenu', blockContext) + return () => document.removeEventListener('contextmenu', blockContext) + }, []) + + const groups = useMemo(() => { + const map = {} + snaps.forEach(s => { + const f = s.folder || '' + if (!map[f]) map[f] = [] + map[f].push(s) + }) + return map + }, [snaps]) + + const allFolders = useMemo(() => { + const set = new Set(extraFolders) + snaps.forEach(s => { if (s.folder) set.add(s.folder) }) + return [...set].sort((a, b) => a.localeCompare(b)) + }, [snaps, extraFolders]) + + const handleSave = () => { + const name = newName.trim() || `Ausschnitt ${snaps.length + 1}` + saveAusschnitt(name) + setNewName('') + } + + const handleAddFolder = () => { + const name = window.prompt('Name für neuen Ordner:') + if (name && name.trim()) addAusschnittFolder(name.trim()) + } + + const ctxItems = (id) => [ + { label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) }, + { label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) }, + { divider: true }, + { label: 'Sichtbarkeit bearbeiten…', icon: 'layers', onClick: () => getAusschnittLayers(id) }, + { divider: true }, + { label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) }, + { label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) }, + { divider: true }, + { label: 'Löschen', icon: 'delete', danger: true, onClick: () => deleteAusschnitt(id) }, + ] + + const folderCtxItems = (folderName) => [ + { label: 'Ordner umbenennen', icon: 'edit', onClick: () => { + const newName = window.prompt('Neuer Ordnername:', folderName) + if (newName && newName.trim() && newName !== folderName) { + snaps.filter(s => s.folder === folderName).forEach(s => setAusschnittFolder(s.id, newName.trim())) + addAusschnittFolder(newName.trim()) + removeAusschnittFolder(folderName) + } + }}, + { divider: true }, + { label: 'Ordner löschen', icon: 'folder_off', danger: true, onClick: () => { + if (window.confirm(`Ordner "${folderName}" löschen? Ausschnitte werden zur Wurzel verschoben.`)) { + removeAusschnittFolder(folderName) + } + }}, + ] + + const handleDrop = (folderName) => (ev) => { + ev.preventDefault() + setDragTarget(null) + const id = ev.dataTransfer.getData('text/plain') || draggingId + if (id) setAusschnittFolder(id, folderName || '') + setDraggingId(null) + } + + const handleDragOver = (folderName) => (ev) => { + ev.preventDefault() + ev.dataTransfer.dropEffect = 'move' + setDragTarget(folderName || 'root') + } + + const handleDragLeave = () => () => setDragTarget(null) + + const renderSnapshot = (s) => ( + restoreAusschnitt(s.id)} + onContextMenu={(ev) => { ev.preventDefault(); setCtxMenu({ x: ev.clientX, y: ev.clientY, id: s.id, kind: 'snap' }) }} + onMenuClick={(ev) => setCtxMenu({ x: ev.clientX, y: ev.clientY, id: s.id, kind: 'snap' })} + onRename={(id, name) => renameAusschnitt(id, name)} + onScaleChange={(id, scale) => setAusschnittScale(id, scale)} + onDragStart={(ev) => { + ev.dataTransfer.setData('text/plain', s.id) + ev.dataTransfer.effectAllowed = 'move' + setDraggingId(s.id) + }} + onDragEnd={() => { setDraggingId(null); setDragTarget(null) }} + /> + ) + + const actions = ( +
+ + +
+ ) + + const rootItems = groups[''] || [] + const isEmpty = snaps.length === 0 && allFolders.length === 0 + + return ( +
+ {/* Fixed Header — wie Layouts/Overrides Pattern */} +
+ + AUSSCHNITTE + + {snaps.length} + {actions} +
+ +
+ {/* Save-Bar als Card */} +
+ setNewName(ev.target.value)} + onKeyDown={(ev) => { if (ev.key === 'Enter') handleSave() }} + placeholder="Name für neuen Ausschnitt…" + style={{ flex: 1, fontSize: 11, fontFamily: 'var(--font)', minWidth: 0 }} + /> + +
+ + {isEmpty ? ( +
+ +
Noch keine Ausschnitte.
+
Oben einen Namen eingeben und klicken.
+
+ ) : ( + <> + {/* Root-Snapshots */} + + {rootItems.map(s => renderSnapshot(s))} + {rootItems.length === 0 && draggingId && ( +
Hier ablegen für Wurzel
+ )} +
+ + {/* Ordner-Cards */} + {allFolders.map(folder => { + const isCollapsed = !!collapsed[folder] + const items = groups[folder] || [] + return ( + setCollapsed(c => ({ ...c, [folder]: !c[folder] }))} + onContextMenu={(ev) => { ev.preventDefault(); setCtxMenu({ x: ev.clientX, y: ev.clientY, name: folder, kind: 'folder' }) }} + onMenuClick={(ev) => setCtxMenu({ x: ev.clientX, y: ev.clientY, name: folder, kind: 'folder' })} + onDragOver={handleDragOver(folder)} + onDragLeave={handleDragLeave()} + onDrop={handleDrop(folder)} + > + {items.length > 0 ? items.map(s => renderSnapshot(s)) : null} + + ) + })} + + )} +
+ Drag & Drop auf Ordner-Card zum Verschieben · Doppelklick auf Name/Maßstab = bearbeiten · ⋮ für Aktionen +
+
+ + {ctxMenu && ( + setCtxMenu(null)} + /> + )} + + {layerDialog && ( + { + updateAusschnittLayers(layerDialog.id, + layers.map(l => ({ id: l.id, visible: l.visible, locked: l.locked }))) + setLayerDialog(null) + }} + onClose={() => setLayerDialog(null)} + onSavePreset={(name, layers) => { + saveLayerPreset(name, layers) + setLayerDialog(d => d ? { ...d, presets: [...d.presets.filter(p => p.name !== name), { name, layers }] } : d) + }} + onDeletePreset={(name) => { + deleteLayerPreset(name) + setLayerDialog(d => d ? { ...d, presets: d.presets.filter(p => p.name !== name) } : d) + }} + /> + )} +
+ ) +} diff --git a/src/DimensionenApp.jsx b/src/DimensionenApp.jsx new file mode 100644 index 0000000..3b542d4 --- /dev/null +++ b/src/DimensionenApp.jsx @@ -0,0 +1,377 @@ +import { useEffect, useState, useRef } from 'react' +import Icon from './components/Icon' +import { + onMessage, notifyReady, + setRefPoint, setCoordSystem, + setDimPosition, setDimDimension, setDimRotationZ, + setCircleRadius, setLineLength, setRectangleDims, +} from './lib/rhinoBridge' + +// ---- Helpers -------------------------------------------------------------- + +const REF_CODES = ['min', 'mid', 'max'] // row/col mapping +const REF_LABELS = { min: 'min', mid: 'mid', max: 'max' } + +function fmtNum(v) { + if (v == null) return '' + if (typeof v !== 'number') return String(v) + // 4 Nachkommastellen, aber unnoetige Nullen weg + return Number(v.toFixed(4)).toString() +} + +// Input-Komponente: zeigt formatierten Wert, sendet onCommit bei Enter/Blur. +// Verhindert Update waehrend des Tippens, damit der Cursor nicht springt. +function NumInput({ value, onCommit, disabled, suffix, width }) { + const [text, setText] = useState(fmtNum(value)) + const [focused, setFocused] = useState(false) + useEffect(() => { if (!focused) setText(fmtNum(value)) }, [value, focused]) + const commit = () => { + const v = parseFloat(text.replace(',', '.')) + if (!isNaN(v) && v !== value) onCommit(v) + else setText(fmtNum(value)) // ungueltig → zurueck auf alten Wert + } + return ( +
+ setText(e.target.value)} + onFocus={(e) => { setFocused(true); e.target.select() }} + onBlur={() => { setFocused(false); commit() }} + onKeyDown={(e) => { + if (e.key === 'Enter') { e.target.blur() } + else if (e.key === 'Escape') { setText(fmtNum(value)); e.target.blur() } + }} + style={{ flex: 1, width: '100%', fontFamily: 'DM Mono, monospace', fontSize: 11, textAlign: 'right' }} + /> + {suffix && {suffix}} +
+ ) +} + +// 9-Punkt-Referenzpunkt-Selektor im Illustrator-Stil: sichtbarer BBox-Rahmen, +// die Punkte sitzen AUF Ecken / Kantenmitten / Zentrum. +function RefPointGrid({ ref, onChange }) { + const SIZE = 26 // Aussenkanten-Quadrat (px) + const DOT = 5 // Punkt-Durchmesser (px) + // Position pro Code: 0% (min), 50% (mid), 100% (max) + const pct = (c) => c === 'min' ? '0%' : c === 'max' ? '100%' : '50%' + return ( +
+ {REF_CODES.map(yc => REF_CODES.map(xc => { + const active = ref.x === xc && ref.y === yc + // yc 'max' = top in user mental model (Vectorworks/Illustrator) + const topPct = yc === 'max' ? '0%' : yc === 'min' ? '100%' : '50%' + return ( +
+ ) +} + +// Z-Referenz-Selektor (Bottom / Mid / Top) — kompakt, nur Icons. +function RefZSelector({ z, onChange }) { + return ( +
+ {[ + { code: 'max', icon: 'vertical_align_top', title: 'Z = Top' }, + { code: 'mid', icon: 'vertical_align_center', title: 'Z = Mid' }, + { code: 'min', icon: 'vertical_align_bottom', title: 'Z = Bottom' }, + ].map(opt => ( + + ))} +
+ ) +} + +// Inline-Label vor einem Input — minimal, knackig +function Field({ label, children, style }) { + return ( +
+ + {label} + + {children} +
+ ) +} + +// ---- Hauptkomponente ------------------------------------------------------ + +export default function DimensionenApp() { + const [state, setState] = useState({ + selection: { count: 0, type: 'none' }, + refPoint: { x: 'min', y: 'min', z: 'mid' }, + coordSystem: 'world', + position: null, + dimensions: null, + shape: null, + planeName: 'Welt', + }) + const [rotationDelta, setRotationDelta] = useState(0) + + useEffect(() => { + onMessage('STATE', (s) => setState((prev) => ({ ...prev, ...s }))) + notifyReady() + }, []) + + const sel = state.selection || { count: 0, type: 'none' } + const ref = state.refPoint || { x: 'min', y: 'min', z: 'mid' } + const pos = state.position + const dims = state.dimensions + const shape = state.shape + const hasSelection = sel.count > 0 && pos != null + + const onRefChange = (next) => setRefPoint(next.x, next.y, next.z) + const onCoordChange = (mode) => setCoordSystem(mode) + + // Selektions-Beschriftung + const selLabel = () => { + if (sel.count === 0) return 'Keine Selektion' + if (sel.count === 1) { + const map = { + curve: 'Kurve', brep: 'Brep', mesh: 'Mesh', extrusion: 'Extrusion', + block: 'Block', point: 'Punkt', text: 'Text', other: 'Objekt', + } + let base = map[sel.type] || '1 Objekt' + if (shape?.type === 'circle') base = 'Kreis' + if (shape?.type === 'rectangle') base = 'Rechteck' + if (shape?.type === 'line') base = 'Linie' + return '1 ' + base + } + return `${sel.count} Objekte` + } + + return ( +
+
+ + {/* Header: Selektions-Info + World/CPlane */} +
+ + {selLabel()} +
+ + +
+
+ + {!hasSelection ? ( +
+ +
Keine Selektion.
+
+ In Rhino ein oder mehrere Objekte auswaehlen. +
+
+ ) : ( + <> + {/* Referenzpunkt — kompakte einzeilige Card */} +
+ + Ref + + + onRefChange({ ...ref, z })} /> +
+ + {/* Position + Abmessungen nebeneinander */} +
+
+
+ Position + + {state.planeName} + +
+ setDimPosition('x', v)} /> + setDimPosition('y', v)} /> + setDimPosition('z', v)} /> +
+
+
+ BBox +
+ setDimDimension('width', v)} /> + setDimDimension('depth', v)} /> + setDimDimension('height', v)} /> +
+
+ + {/* Shape-spezifisch */} + {shape && ( +
+
+ {shape.type === 'circle' && 'Kreis'} + {shape.type === 'rectangle' && 'Rechteck'} + {shape.type === 'line' && 'Linie'} +
+ {shape.type === 'circle' && ( + setCircleRadius(v)} /> + )} + {shape.type === 'rectangle' && ( +
+ + setRectangleDims(v, shape.height)} /> + + + setRectangleDims(shape.width, v)} /> + +
+ )} + {shape.type === 'line' && ( +
+ + setLineLength(v)} /> + + + {}} disabled suffix="°" /> + +
+ )} +
+ )} + + {/* Rotation — kompakt einzeilig */} +
+ + Drehen + +
+ +
+ + {[-90, -45, 45, 90].map(a => ( + + ))} +
+ + )} +
+
+ ) +} diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx new file mode 100644 index 0000000..7197176 --- /dev/null +++ b/src/ElementeApp.jsx @@ -0,0 +1,1391 @@ +import { useEffect, useRef, useState } from 'react' +import Icon from './components/Icon' +import { + onMessage, notifyReady, + listElemente, createWall, createDecke, createDach, + createFenster, createTuer, createTreppe, + updateElement, deleteElement, regenerateAllElements, +} from './lib/rhinoBridge' + +const labelXs = { + fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', + letterSpacing: '0.06em', textTransform: 'uppercase', +} + +function fmtNum(v) { + if (v == null || v === '') return '' + const n = Number(v) + if (Number.isNaN(n)) return String(v) + return Number(n.toFixed(4)).toString() +} + +function ReferenzSelector({ value, onChange }) { + const opts = [ + { code: 'left', label: 'Links', hint: 'Achse auf linker Aussenseite' }, + { code: 'mid', label: 'Mittig', hint: 'Achse zentriert (Standard)' }, + { code: 'right', label: 'Rechts', hint: 'Achse auf rechter Aussenseite' }, + ] + return ( +
+ {opts.map(o => ( + + ))} +
+ ) +} + +// Pill-Button — kompakt, Icon + Label horizontal, abgerundet +function PillButton({ icon, label, hint, onClick, onContextMenu, disabled, + hasMenu, badge }) { + return ( + + ) +} + +// Vertikale Kategorie-Gruppe mit Label + Pills, die wrappen +function PillGroup({ label, children }) { + return ( +
+ + {label} + +
+ {children} +
+
+ ) +} + +// Popup-Menue (relativ positioniert) fuer Untertypen wie Treppen-Art +function PopupMenu({ items, onClose }) { + useEffect(() => { + const onDocClick = () => onClose() + document.addEventListener('click', onDocClick) + return () => document.removeEventListener('click', onDocClick) + }, [onClose]) + return ( +
+ {items.map((it, i) => ( + + ))} +
+ ) +} + +const KIND_META = { + wand: { icon: 'view_week', label: 'Wand', color: '#a8b8c8' }, + decke: { icon: 'layers', label: 'Decke', color: '#b8a890' }, + dach: { icon: 'roofing', label: 'Dach', color: '#c89878' }, + fenster: { icon: 'window', label: 'Fenster', color: '#90b8d0' }, + tuer: { icon: 'sensor_door', label: 'Tuer', color: '#c8a878' }, + treppe: { icon: 'stairs', label: 'Treppe', color: '#a0c0a0' }, +} + +function ElementList({ elements }) { + // Gruppiert nach kind, dann nach geschoss-Reihenfolge wie sie reinkommen + const grouped = {} + for (const el of elements) { + const k = el.kind || 'unknown' + if (!grouped[k]) grouped[k] = [] + grouped[k].push(el) + } + const kindOrder = ['wand', 'decke', 'dach', 'fenster', 'tuer', 'treppe'] + return ( +
+
+ Alle Elemente + {elements.length} +
+ {kindOrder.map(k => { + const arr = grouped[k] + if (!arr || arr.length === 0) return null + const meta = KIND_META[k] || { icon: 'help', label: k, color: '#888' } + return ( +
+
+ + {meta.label} + · + {arr.length} +
+ {arr.map(el => ( + + ))} +
+ ) + })} +
+ ) +} + +function ElementListRow({ el, meta }) { + const secondary = (() => { + if (el.kind === 'fenster' || el.kind === 'tuer') + return `${fmtNum(el.breite)}×${fmtNum(el.hoehe)} m` + if (el.kind === 'treppe') + return `${el.nStufen} St · H ${fmtNum(el.ok - el.uk)} m` + if (el.kind === 'dach' && el.neigung != null) + return `d ${fmtNum(el.dicke)} · ${fmtNum(el.neigung)}°` + return `d ${fmtNum(el.dicke)} m` + })() + const tertiary = (() => { + if (el.kind === 'fenster') + return `Br ${fmtNum(el.brueest)}` + if (el.kind === 'tuer' || el.kind === 'treppe') + return '' + return `UK ${fmtNum(el.uk)} · OK ${fmtNum(el.ok)}` + })() + return ( +
+ + {el.geschossName || '—'} + + + {secondary} + + {tertiary && ( + + {tertiary} + + )} +
+ ) +} + + +function NeuesElementSection({ noGeschoss, activeName }) { + const [treppeMenuOpen, setTreppeMenuOpen] = useState(false) + const treppeWrapperRef = useRef(null) + const dis = noGeschoss + const baseHint = (label) => + noGeschoss ? 'Erst im Ebenen-Manager ein Geschoss aktivieren' + : `${label} auf ${activeName}` + + const openTreppeMenu = (e) => { + e.preventDefault() + setTreppeMenuOpen(true) + } + + const treppeItems = [ + { icon: 'stairs', label: 'Gerade Treppe', + hint: 'Lauflinie mit 2 Punkten', + onClick: () => createTreppe({ treppeArt: 'gerade' }) }, + { icon: 'turn_right', label: 'L-Treppe', + hint: '3 Punkte: Start, Podest-Ecke, Ende', + onClick: () => createTreppe({ treppeArt: 'l' }) }, + { icon: 'rotate_right', label: 'Wendeltreppe', + hint: '3 Punkte: Mittelpunkt, Start-Lauflinie, End-Lauflinie', + onClick: () => createTreppe({ treppeArt: 'wendel' }) }, + ] + + return ( +
+
+ Neues Element +
+ + {noGeschoss ? 'Kein Geschoss aktiv' : 'auf'} + + {!noGeschoss && ( + + {activeName} + + )} +
+ + + createWall({ geschoss: '' })} /> + createDecke({ geschoss: '' })} /> + createDach({ geschoss: '' })} /> + + + + createFenster({})} /> + createTuer({})} /> + + + +
+ createTreppe({ treppeArt: 'gerade' })} + onContextMenu={openTreppeMenu} /> + {treppeMenuOpen && ( + setTreppeMenuOpen(false)} /> + )} +
+
+
+ ) +} + + +export default function ElementeApp() { + const [state, setState] = useState({ + elements: [], geschosse: [], selection: null, + activeGeschoss: '', activeGeschossName: '', + }) + // Defaults werden vom Backend (sticky) verwaltet — letzte Werte aus + // dem Rhino-Prompt bleiben fuer den naechsten Klick erhalten. + + useEffect(() => { + onMessage('STATE', (s) => setState(prev => ({ ...prev, ...s }))) + notifyReady() + }, []) + + const elements = state.elements || [] + const geschosse = state.geschosse || [] + const selected = elements.find(el => el.id === state.selection) + const activeName = state.activeGeschossName || '' + const noGeschoss = !state.activeGeschoss + + return ( +
+ {/* Header */} +
+ ELEMENTE + {elements.length} + + +
+ +
+ {/* Element-Toolbar: kategorisierte Pills */} + + + + {/* Properties */} + {selected ? ( + selected.kind === 'wand' ? ( + updateElement(selected.id, p)} + onDelete={() => { if (window.confirm('Wand loeschen?')) deleteElement(selected.id) }} /> + ) : selected.kind === 'decke' ? ( + updateElement(selected.id, p)} + onDelete={() => { if (window.confirm('Decke loeschen?')) deleteElement(selected.id) }} /> + ) : selected.kind === 'dach' ? ( + updateElement(selected.id, p)} + onDelete={() => { if (window.confirm('Dach loeschen?')) deleteElement(selected.id) }} /> + ) : selected.kind === 'treppe' ? ( + updateElement(selected.id, p)} + onDelete={() => { if (window.confirm('Treppe loeschen?')) deleteElement(selected.id) }} /> + ) : ( + updateElement(selected.id, p)} + onDelete={() => { + const label = selected.kind === 'fenster' ? 'Fenster' : 'Tuer' + if (window.confirm(`${label} loeschen?`)) deleteElement(selected.id) + }} /> + ) + ) : ( +
+ +
Kein Element selektiert.
+
+ Eine Wand-Achse oder Decken-Outline in Rhino auswaehlen. +
+
+ )} + + {/* Liste aller Elemente */} + {elements.length > 0 && ( + + )} +
+
+ ) +} + +function WallProperties({ wall, geschosse, onUpdate, onDelete }) { + const [dicke, setDicke] = useState(String(wall.dicke)) + const [ukOver, setUkOver] = useState(wall.ukOverride) + const [okOver, setOkOver] = useState(wall.okOverride) + useEffect(() => { + setDicke(String(wall.dicke)) + setUkOver(wall.ukOverride) + setOkOver(wall.okOverride) + }, [wall.id, wall.dicke, wall.ukOverride, wall.okOverride]) + + const ukAuto = ukOver === '' || ukOver == null + const okAuto = okOver === '' || okOver == null + + return ( +
+
+ + + Wand · {wall.geschossName} + + +
+ +
+ Geschoss + +
+ +
+ Dicke + setDicke(e.target.value)} + onBlur={() => { + const v = parseFloat(dicke) + if (v > 0 && v !== wall.dicke) onUpdate({ dicke: v }) + else setDicke(String(wall.dicke)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> +
+ +
+ Referenz + onUpdate({ referenz: v })} /> +
+ + onUpdate({ ukOverride: ukAuto ? wall.uk : '' })} + onCommit={() => { + if (ukAuto) return + const v = parseFloat(ukOver) + if (!Number.isNaN(v)) onUpdate({ ukOverride: v }) + }} /> + + onUpdate({ okOverride: okAuto ? wall.ok : '' })} + onCommit={() => { + if (okAuto) return + const v = parseFloat(okOver) + if (!Number.isNaN(v)) onUpdate({ okOverride: v }) + }} /> +
+ ) +} + +function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) { + const [dicke, setDicke] = useState(String(decke.dicke)) + const [ukOver, setUkOver] = useState(decke.ukOverride) + const [okOver, setOkOver] = useState(decke.okOverride) + useEffect(() => { + setDicke(String(decke.dicke)) + setUkOver(decke.ukOverride) + setOkOver(decke.okOverride) + }, [decke.id, decke.dicke, decke.ukOverride, decke.okOverride]) + + const ukAuto = ukOver === '' || ukOver == null + const okAuto = okOver === '' || okOver == null + + return ( +
+
+ + + Decke · {decke.geschossName} + + +
+ +
+ Geschoss + +
+ +
+ Dicke + setDicke(e.target.value)} + onBlur={() => { + const v = parseFloat(dicke) + if (v > 0 && v !== decke.dicke) onUpdate({ dicke: v }) + else setDicke(String(decke.dicke)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> +
+ + onUpdate({ ukOverride: ukAuto ? decke.uk : '' })} + onCommit={() => { + if (ukAuto) return + const v = parseFloat(ukOver) + if (!Number.isNaN(v)) onUpdate({ ukOverride: v }) + }} /> + + onUpdate({ okOverride: okAuto ? decke.ok : '' })} + onCommit={() => { + if (okAuto) return + const v = parseFloat(okOver) + if (!Number.isNaN(v)) onUpdate({ okOverride: v }) + }} /> +
+ ) +} + +function DachProperties({ dach, geschosse, onUpdate, onDelete }) { + const [dicke, setDicke] = useState(String(dach.dicke)) + const [neigung, setNeigung] = useState(String(dach.neigung ?? 30)) + const [eaveIdx, setEaveIdx] = useState(String(dach.eaveIdx ?? 0)) + const [ukOver, setUkOver] = useState(dach.ukOverride) + useEffect(() => { + setDicke(String(dach.dicke)) + setNeigung(String(dach.neigung ?? 30)) + setEaveIdx(String(dach.eaveIdx ?? 0)) + setUkOver(dach.ukOverride) + }, [dach.id, dach.dicke, dach.neigung, dach.eaveIdx, dach.ukOverride]) + const dachTyp = dach.dachTyp || 'pult' + + const ukAuto = ukOver === '' || ukOver == null + + return ( +
+
+ + + {dachTyp === 'sattel' ? 'Satteldach' : dachTyp === 'walm' ? 'Walmdach' : 'Pultdach'} · {dach.geschossName} + + +
+ + {/* Dach-Typ */} +
+ Typ +
+ {[ + { code: 'pult', label: 'Pult' }, + { code: 'sattel', label: 'Sattel' }, + { code: 'walm', label: 'Walm' }, + { code: 'mansarde', label: 'Mansarde' }, + ].map(o => ( + + ))} +
+
+ +
+ Geschoss + +
+ +
+ Dicke + setDicke(e.target.value)} + onBlur={() => { + const v = parseFloat(dicke) + if (v > 0 && v !== dach.dicke) onUpdate({ dicke: v }) + else setDicke(String(dach.dicke)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> +
+ +
+ Neigung + setNeigung(e.target.value)} + onBlur={() => { + const v = parseFloat(neigung) + if (!Number.isNaN(v) && v >= 0 && v < 90) onUpdate({ neigung: v }) + else setNeigung(String(dach.neigung ?? 30)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + ° +
+ + {dachTyp === 'pult' && ( +
+ + Traufe + + setEaveIdx(e.target.value)} + onBlur={() => { + const v = parseInt(eaveIdx, 10) + if (!Number.isNaN(v) && v >= 0) onUpdate({ eaveIdx: v }) + else setEaveIdx(String(dach.eaveIdx ?? 0)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + Kante +
+ )} + + {/* Mansarde-spezifisch: Variante + untere Neigung + Knick-Hoehe */} + {dachTyp === 'mansarde' && ( + <> +
+ Variante +
+ {[ + { 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: 'walm_giebel', label: 'W-G', hint: 'Unten Walm (Knick rundum), oben Giebel mit First ueber voller Laenge' }, + ].map(o => ( + + ))} +
+
+ + + )} + + onUpdate({ ukOverride: ukAuto ? dach.uk : '' })} + onCommit={() => { + if (ukAuto) return + const v = parseFloat(ukOver) + if (!Number.isNaN(v)) onUpdate({ ukOverride: v }) + }} /> +
+ ) +} + +function MansardeFields({ dach, onUpdate }) { + const [nu, setNu] = useState(String(dach.neigungUnten ?? 60)) + const [kh, setKh] = useState(String(dach.knickH ?? 2.0)) + useEffect(() => { + setNu(String(dach.neigungUnten ?? 60)) + setKh(String(dach.knickH ?? 2.0)) + }, [dach.id, dach.neigungUnten, dach.knickH]) + return ( + <> +
+ + Steil + + setNu(e.target.value)} + onBlur={() => { + const v = parseFloat(nu) + if (!Number.isNaN(v) && v > 0 && v < 90) onUpdate({ neigungUnten: v }) + else setNu(String(dach.neigungUnten ?? 60)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + ° +
+
+ + Knick H + + setKh(e.target.value)} + onBlur={() => { + const v = parseFloat(kh) + if (!Number.isNaN(v) && v > 0) onUpdate({ knickH: v }) + else setKh(String(dach.knickH ?? 2.0)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + m +
+ + ) +} + +const SIMS_OPTIONS = [ + { code: 'ohne', label: 'ohne' }, + { code: 'schmal', label: 'schmal' }, + { code: 'standard', label: 'standard' }, + { code: 'breit', label: 'breit' }, +] + +const RAHMEN_POS_OPTIONS = [ + { code: 'aussen', label: 'aussen', hint: 'Rahmen buendig mit Aussenflaeche' }, + { code: 'mid', label: 'mittig', hint: 'Rahmen mittig im Wandquerschnitt' }, + { code: 'innen', label: 'innen', hint: 'Rahmen buendig mit Innenflaeche' }, +] + +const OEFF_REFERENZ_OPTIONS = [ + { code: 'links', label: 'Links', hint: 'Klick-Punkt am linken Rand — Oeffnung extendiert nach rechts (+tan der Wand-Achse)' }, + { code: 'mid', label: 'Mittig', hint: 'Klick-Punkt mittig in der Oeffnung (Standard)' }, + { code: 'rechts', label: 'Rechts', hint: 'Klick-Punkt am rechten Rand — Oeffnung extendiert nach links (-tan)' }, +] + +function SollRow({ label, value, unit, soll, sollKey, onUpdateSoll, readOnly }) { + // soll[sollKey] = [lo, hi, on] + const lo = soll?.[sollKey]?.[0] ?? 0 + const hi = soll?.[sollKey]?.[1] ?? 0 + const on = soll?.[sollKey]?.[2] ?? true + const inRange = value >= lo && value <= hi + const valueColor = !on ? 'var(--text-muted)' + : inRange ? 'var(--accent)' + : '#c87050' + const [loStr, setLoStr] = useState(String(lo)) + const [hiStr, setHiStr] = useState(String(hi)) + useEffect(() => { + setLoStr(String(lo)) + setHiStr(String(hi)) + }, [lo, hi]) + + const commit = (k, val, setBack, def) => { + const v = parseFloat(val) + if (!Number.isNaN(v) && v > 0) { + const next = [...soll[sollKey]] + next[k] = v + onUpdateSoll({ ...soll, [sollKey]: next }) + } else setBack(String(def)) + } + + const toggle = () => { + const next = [...soll[sollKey]] + next[2] = !next[2] + onUpdateSoll({ ...soll, [sollKey]: next }) + } + + return ( +
+ + {label} + + {fmtNum(value)} {unit} + + {readOnly ? ( + + {fmtNum(lo)}–{fmtNum(hi)} + + ) : ( + <> + Soll + setLoStr(e.target.value)} + onBlur={() => commit(0, loStr, setLoStr, lo)} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + disabled={!on} + style={{ + width: 38, fontSize: 9, fontFamily: 'DM Mono, monospace', + padding: '1px 3px', background: 'transparent', + border: '1px solid var(--border-light)', + color: on ? 'var(--text-primary)' : 'var(--text-muted)', + }} /> + + setHiStr(e.target.value)} + onBlur={() => commit(1, hiStr, setHiStr, hi)} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + disabled={!on} + style={{ + width: 38, fontSize: 9, fontFamily: 'DM Mono, monospace', + padding: '1px 3px', background: 'transparent', + border: '1px solid var(--border-light)', + color: on ? 'var(--text-primary)' : 'var(--text-muted)', + }} /> + + )} +
+ ) +} + +const DEFAULT_TREPPE_SOLL = { + s: [0.15, 0.20, true], + a: [0.21, 0.35, true], + sa: [0.60, 0.65, true], +} + +function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) { + const [breite, setBreite] = useState(String(treppe.breite ?? 1.0)) + const [nStufen, setNStufen] = useState(String(treppe.nStufen ?? 15)) + const [laufD, setLaufD] = useState(String(treppe.laufD ?? 0.18)) + const [hStr, setHStr] = useState('') + useEffect(() => { + setBreite(String(treppe.breite ?? 1.0)) + setNStufen(String(treppe.nStufen ?? 15)) + setLaufD(String(treppe.laufD ?? 0.18)) + }, [treppe.id, treppe.breite, treppe.nStufen, treppe.laufD]) + + const H = (treppe.ok ?? 0) - (treppe.uk ?? 0) + const N = treppe.nStufen ?? 15 + const L = treppe.laufLen ?? 0 + const S = N > 0 ? H / N : 0 + const A = N > 0 ? L / N : 0 + const sa = 2 * S + A + const soll = treppe.soll || DEFAULT_TREPPE_SOLL + const hasHOver = treppe.hOver != null && treppe.hOver !== '' + useEffect(() => { + setHStr(hasHOver ? String(treppe.hOver) : fmtNum(H)) + }, [treppe.id, treppe.hOver, H, hasHOver]) + + const allOK = ( + (!soll.s[2] || (S >= soll.s[0] && S <= soll.s[1])) && + (!soll.a[2] || (A >= soll.a[0] && A <= soll.a[1])) && + (!soll.sa[2] || (sa >= soll.sa[0] && sa <= soll.sa[1])) + ) + + const onUpdateSoll = (newSoll) => { + onUpdate({ soll: newSoll }) + } + const onCommitH = () => { + const v = parseFloat(hStr) + if (!Number.isNaN(v) && v > 0 && Math.abs(v - H) > 1e-5) { + // User hat H ueberschrieben → Ziel auf "eigene" + onUpdate({ hOver: v, geschossEnd: '' }) + } + } + const onClearHOver = () => { + onUpdate({ hOver: '' }) + } + + const ref = treppe.treppeReferenz ?? 'mid' + const REF_OPTIONS = [ + { code: 'links', label: 'Links' }, + { code: 'mid', label: 'Mittig' }, + { code: 'rechts', label: 'Rechts' }, + ] + const modus = treppe.treppeModus ?? 'flach' + const MODUS_OPTIONS = [ + { 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: 'plattenrand', label: 'gestuft', hint: 'Plattenunterseite folgt den Stufen, vertikal versetzt' }, + ] + + return ( +
+
+ + + Treppe · {treppe.geschossName} → {treppe.geschossEndName || '(auto)'} + + +
+ +
+ Start + +
+ +
+ Ziel + +
+ +
+ Breite + setBreite(e.target.value)} + onBlur={() => { + const v = parseFloat(breite) + if (!Number.isNaN(v) && v >= 0.3) onUpdate({ breite: v }) + else setBreite(String(treppe.breite)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + m +
+ +
+ Stufen + setNStufen(e.target.value)} + onBlur={() => { + const v = parseInt(nStufen, 10) + if (Number.isFinite(v) && v >= 2 && v <= 40) onUpdate({ nStufen: v }) + else setNStufen(String(treppe.nStufen)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + × +
+ +
+ Lage +
+ {REF_OPTIONS.map(o => ( + + ))} +
+
+ +
+ + {/* Unterseite-Modus */} +
+ + Unten + +
+ {MODUS_OPTIONS.map(o => ( + + ))} +
+
+ + {/* Lauf-Plattendicke (nur fuer flach + plattenrand relevant) */} + {modus !== 'massiv' && ( +
+ + Platte + + setLaufD(e.target.value)} + onBlur={() => { + const v = parseFloat(laufD) + if (!Number.isNaN(v) && v > 0) onUpdate({ laufD: v }) + else setLaufD(String(treppe.laufD)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + m +
+ )} + + {/* Schrittmass-Tabelle: H (editierbar), S, A, 2S+A mit on/off + range */} +
+ {/* H — editierbar; aendert Hoehe und kippt Ziel auf "eigene" */} +
+ + H + setHStr(e.target.value)} + onBlur={onCommitH} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ + width: 56, fontSize: 10, fontFamily: 'DM Mono, monospace', + padding: '1px 3px', background: 'transparent', + border: '1px solid ' + (hasHOver ? 'var(--accent)' : 'var(--border-light)'), + color: 'var(--text-primary)', + }} /> + m + {hasHOver && ( + + )} +
+ + + +
+
+ ) +} + +function OeffnungProperties({ oeff, onUpdate, onDelete }) { + const isFenster = oeff.kind === 'fenster' + const label = isFenster ? 'Fenster' : 'Tuer' + const icon = isFenster ? 'window' : 'sensor_door' + 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 [brueest, setBrueest] = useState(String(oeff.brueest ?? 0.9)) + const [rahmenB, setRahmenB] = useState(String(oeff.rahmenB ?? 0.06)) + const [rahmenTiefe, setRahmenTiefe] = useState(String(oeff.rahmenTiefe ?? 0.08)) + useEffect(() => { + setBreite(String(oeff.breite ?? (isFenster ? 1.2 : 0.9))) + setHoehe(String(oeff.hoehe ?? (isFenster ? 1.4 : 2.1))) + setBrueest(String(oeff.brueest ?? 0.9)) + setRahmenB(String(oeff.rahmenB ?? 0.06)) + setRahmenTiefe(String(oeff.rahmenTiefe ?? 0.08)) + }, [oeff.id, oeff.breite, oeff.hoehe, oeff.brueest, oeff.rahmenB, + oeff.rahmenTiefe, isFenster]) + + const commit = (key, val, setBack, def) => { + const v = parseFloat(val) + if (!Number.isNaN(v) && v > 0) onUpdate({ [key]: v }) + else setBack(String(def)) + } + + const fluegel = oeff.fluegel ?? 1 + const rahmenPos = oeff.rahmenPos ?? 'mid' + const simsAus = oeff.simsAus ?? 'ohne' + const simsIn = oeff.simsIn ?? 'ohne' + + return ( +
+
+ + + {label} · {oeff.geschossName} + + +
+ +
+ Breite + setBreite(e.target.value)} + onBlur={() => commit('breite', breite, setBreite, oeff.breite)} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + m +
+ +
+ Hoehe + setHoehe(e.target.value)} + onBlur={() => commit('hoehe', hoehe, setHoehe, oeff.hoehe)} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + m +
+ + {isFenster && ( +
+ + Bruest. + + setBrueest(e.target.value)} + onBlur={() => { + const v = parseFloat(brueest) + if (!Number.isNaN(v) && v >= 0) onUpdate({ brueest: v }) + else setBrueest(String(oeff.brueest)) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + m +
+ )} + + {/* Referenz-Lage: wo sitzt der Klick-Punkt in der Oeffnung */} +
+ + Ref + +
+ {OEFF_REFERENZ_OPTIONS.map(o => ( + + ))} +
+
+ +
+ + {/* Rahmen-Profil (Breite × Tiefe) — beide Felder gleich breit */} +
+ + Rahmen + + setRahmenB(e.target.value)} + onBlur={() => commit('rahmenB', rahmenB, setRahmenB, oeff.rahmenB)} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + title="Profil-Breite (sichtbar in der Fassade)" + style={{ flex: '1 1 0', minWidth: 0, width: 0, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + × + setRahmenTiefe(e.target.value)} + onBlur={() => commit('rahmenTiefe', rahmenTiefe, setRahmenTiefe, oeff.rahmenTiefe)} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + title="Rahmen-Tiefe (Lage in der Wand)" + style={{ flex: '1 1 0', minWidth: 0, width: 0, fontSize: 11, fontFamily: 'DM Mono, monospace' }} /> + m +
+ + {/* Rahmen-Lage im Wandquerschnitt */} +
+ + Lage + +
+ {RAHMEN_POS_OPTIONS.map(o => ( + + ))} +
+
+ + {/* Fluegel-Anzahl */} +
+ + Fluegel + +
+ {[1, 2, 3, 4].map(n => ( + + ))} +
+
+ + {/* Sims-Stile (aussen / innen) — nur fuer Fenster */} + {isFenster && ( + <> +
+ + Sims a. + + +
+
+ + Sims i. + + +
+ + )} + + {/* Glas-Toggle: bei Tueren ersetzt Glas das Tuerblatt (verglaste Tuer) */} +
+ +
+
+ ) +} + + +// Wiederverwendete UI fuer UK/OK-Felder mit Auto/Custom-Toggle +function AutoOverrideField({ label, auto, autoValue, rawValue, onChangeRaw, onToggle, onCommit }) { + return ( +
+ {label} + + onChangeRaw(e.target.value)} + onBlur={onCommit} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace', + opacity: auto ? 0.6 : 1 }} /> +
+ ) +} diff --git a/src/GestaltungApp.jsx b/src/GestaltungApp.jsx new file mode 100644 index 0000000..9518a58 --- /dev/null +++ b/src/GestaltungApp.jsx @@ -0,0 +1,516 @@ +import { useState, useEffect, useRef } from 'react' +import Icon from './components/Icon' +import { + onMessage, notifyReady, + requestSelection, setColorSource, setLwSource, setLinetypeSource, setLinetypeScale, setFill, +} from './lib/rhinoBridge' + +const LW_PRESETS = [0.02, 0.10, 0.13, 0.18, 0.25, 0.35, 0.50, 0.70, 1.00] + +// --------------------------------------------------------------------------- + +function SectionHead({ title }) { + return ( +
+ + {title} + +
+
+ ) +} + +/** Source-Dropdown im Vectorworks-Stil: Link-Icon + Label + Caret */ +function SourceSelect({ source, onChange, overrideLabel = 'Eigene' }) { + return ( +
+ + - + Pille (index.css) damit's optisch konsistent mit dem Fill-Dropdown + wirkt. Nur paddingLeft ueberschreiben wegen Link-Icon. */ + }} + > + + + +
+ ) +} + +function ColorBar({ color, onChange, height = 22 }) { + // Readonly-Variante als reines Anzeige-Rechteck + if (!onChange) { + return ( +
+ ) + } + // Editierbar: