Compare commits
203 Commits
9dc191be4f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d558fac2c3 | |||
| e8da519d29 | |||
| cd87a0b7f4 | |||
| c0624c0a62 | |||
| e531217cb7 | |||
| b6ba83eb5d | |||
| 05a289dd65 | |||
| 82d0939a18 | |||
| 220a1e2bb3 | |||
| b9a2124026 | |||
| 84ff943f92 | |||
| 24f6b76f06 | |||
| 9fcada260e | |||
| b9f661cdb3 | |||
| 375487c10c | |||
| 92b4baa285 | |||
| 18443b60c3 | |||
| dcfafb18d1 | |||
| 118bc51cc5 | |||
| df56a54b66 | |||
| 9cce8199c3 | |||
| e50134ce32 | |||
| c81d2c0c43 | |||
| 26214a704d | |||
| 3e54fa46a6 | |||
| 17ff7a8017 | |||
| 9999f3d0ad | |||
| 3609236da9 | |||
| e9e727c66f | |||
| bc87ae1acc | |||
| 0171785b42 | |||
| f011e2ca94 | |||
| 5fdad504da | |||
| 0cc06364e8 | |||
| 5c7611ad40 | |||
| 66971eaa7a | |||
| 98824c1680 | |||
| 080659ab95 | |||
| 250853d7d0 | |||
| 18d6d98e07 | |||
| 7930705d01 | |||
| d8966cc035 | |||
| e406e8d9b2 | |||
| bb64e4d41e | |||
| 6060c74b17 | |||
| 970281e10a | |||
| bcf7d557b1 | |||
| d9589e99f5 | |||
| f8d1cfe3fe | |||
| 264327432d | |||
| 6a13ede6b7 | |||
| edaf83229b | |||
| 6fee7bd143 | |||
| 61923e1b2b | |||
| e2d66a5d64 | |||
| 1c3b0f3919 | |||
| 2a838aee93 | |||
| f457db93e7 | |||
| 975071c995 | |||
| ac7b2f2ee5 | |||
| 2386366566 | |||
| 7fbda8c289 | |||
| f208e7fc00 | |||
| 662ce87e98 | |||
| 46970fd4f0 | |||
| eff0878f53 | |||
| 3c28d2e29c | |||
| f1860ae85d | |||
| da0fd365f2 | |||
| cd626b0707 | |||
| dd5ccec881 | |||
| 95678d4394 | |||
| 067cb56584 | |||
| b10760a704 | |||
| e56ee2cb8f | |||
| 238d7d062b | |||
| 01b6501a0c | |||
| 02a00a9b4a | |||
| 13a5e1eb7a | |||
| e1b63aa4e6 | |||
| 827bd8d4d7 | |||
| 68b9d14453 | |||
| c993935b17 | |||
| de57c320c2 | |||
| 8184f559fc | |||
| 8f691e37c4 | |||
| a597b58c93 | |||
| 8d3b3af882 | |||
| ad56d9e930 | |||
| c63bdd5bc1 | |||
| 0bf891641f | |||
| f760d1c54b | |||
| f60d643bb7 | |||
| c050b9aeb6 | |||
| a308ba62d2 | |||
| ee01c7ebdc | |||
| 736325fba1 | |||
| 059cbf8d4d | |||
| 3277f61ced | |||
| 3dc6e31374 | |||
| 655d368c92 | |||
| 0c5f8055a5 | |||
| 9ae8574ab0 | |||
| 15fb0a6037 | |||
| d5bcee2157 | |||
| 26c7d9e67d | |||
| ae80185064 | |||
| 6b3421e7af | |||
| de6f84346c | |||
| 29699b5eda | |||
| 5abf1c0137 | |||
| 91bc03184e | |||
| 2d48a6ed3a | |||
| 211078d229 | |||
| 3bd949e590 | |||
| 1596bbd941 | |||
| 51987dcc38 | |||
| f4404db64a | |||
| e7a1753519 | |||
| 54aa1c9e84 | |||
| ab0ecfbf14 | |||
| b047d0aa4b | |||
| cc0e6d814e | |||
| 9eece87cc4 | |||
| 9256e5866e | |||
| e9f0e255a0 | |||
| 9f257b83e6 | |||
| 38041ab6a0 | |||
| 6fce00343c | |||
| 38314bcc6f | |||
| 79c8392c2c | |||
| c16f5ea740 | |||
| 872832a3cc | |||
| 4a31652f78 | |||
| 205c626a5a | |||
| 2252ffd2f9 | |||
| 817455bbba | |||
| e2c13e7844 | |||
| 3f5f48cb2c | |||
| b0badbbda6 | |||
| ea8292ed14 | |||
| ea4c891b98 | |||
| 0182497963 | |||
| 02b5fbfde4 | |||
| b78a95caaa | |||
| 1d1cd10a0b | |||
| 700cc11956 | |||
| 66d6e63959 | |||
| cf40c03602 | |||
| e19bbafe38 | |||
| 8ad9e23838 | |||
| 536d42f097 | |||
| 2e6dc44923 | |||
| c8286b931b | |||
| 76572968ca | |||
| c22aef6b65 | |||
| 2ee4688fe3 | |||
| 0b4b25cf47 | |||
| b69dd8e279 | |||
| de1fc887f5 | |||
| 9cde41b686 | |||
| b14eb1e5dd | |||
| 68411a0ce9 | |||
| 5fd6aefd34 | |||
| 72e24fd512 | |||
| b425421fdd | |||
| 1e6bc68156 | |||
| 85f09390bc | |||
| afb59b6626 | |||
| 4111f12f32 | |||
| 41b6f8ac51 | |||
| ee4d4876dd | |||
| 5f5ed531b5 | |||
| 0caa0f9813 | |||
| ce81d42916 | |||
| 222b00c113 | |||
| 95031ee2c0 | |||
| e3918cb155 | |||
| 42d9c1e27b | |||
| b1b2090b3e | |||
| 1ba0bda429 | |||
| e6a39531f4 | |||
| d6c09d22f7 | |||
| fcbc97b608 | |||
| 581f366437 | |||
| f14f84ca36 | |||
| d63bca1ad8 | |||
| 10690f4514 | |||
| c4c9e56b2c | |||
| cbabc12064 | |||
| e96de793a9 | |||
| a458b4c47d | |||
| 53fea10cba | |||
| 15185568ce | |||
| 82bd15a074 | |||
| 0978d9fc2e | |||
| 2a75b1da93 | |||
| d3984ba501 | |||
| 8a67b9f9d1 | |||
| 8f5084b085 | |||
| 2dde46cb85 | |||
| 961b3c0396 | |||
| 1180d7bedf |
@@ -26,6 +26,12 @@ __pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# Rhino-Testdateien (rhino/-Ordner)
|
||||
rhino/*.3dm
|
||||
rhino/*.3dm.thumb.png
|
||||
rhino/*.3dmbak
|
||||
rhino/dossier.project.json
|
||||
|
||||
# Claude Code
|
||||
.claude/
|
||||
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
# Dossier — Architektur
|
||||
|
||||
Stand: 2026-05-17. Dieses Dokument beschreibt wie die Module zusammenspielen,
|
||||
welche Konventionen gelten und wo bekannte Schwachstellen liegen.
|
||||
|
||||
> ⚠️ **Runtime-Realität — Migration in Arbeit:** Trotz `# ! python3`
|
||||
> Shebangs in allen Files läuft der Plugin-Code aktuell als **IronPython 2.7**
|
||||
> unter .NET 8 auf Mac Rhino 8.31 — verifiziert via `sys.version`. Migration
|
||||
> zu Rhinos neuer CPython-3-Engine läuft (siehe CLAUDE.md für Stand). Bis
|
||||
> dahin gelten IPy-2.7-Idiome im Code: `__builtin__`, kein f-string-Vertrauen,
|
||||
> `print` mit/ohne Klammern gemischt, `import System` direkt (statt `clr.AddReference`).
|
||||
|
||||
**Bei jeder Code-Änderung in `rhino/` zuerst dieses Dokument lesen.** Wenn
|
||||
sich Patterns hier ändern, dieses Dokument mit-aktualisieren — sonst rotted
|
||||
es.
|
||||
|
||||
---
|
||||
|
||||
## 1. Module-Map (`rhino/`)
|
||||
|
||||
| Modul | LOC | Rolle | Hängt ab von |
|
||||
|---|---:|---|---|
|
||||
| `panel_base.py` | 697 | **Fundament**: BaseBridge, WebView-IO, Panel-Registration, Icons, Legacy-Migration | — |
|
||||
| `rhinopanel.py` (EBENEN) | 798 | Zeichnungsebenen (Layer-Hierarchie, Presets) | panel_base, layer_builder, massstab (sticky) |
|
||||
| `elemente.py` (ELEMENTE) | **7244** | Smart Elements: Wände, Decken, Öffnungen, Treppen, Tragwerk, Räume (SIA-416) | panel_base, overrides+rhinopanel (sticky) |
|
||||
| `gestaltung.py` (GESTALTUNG) | 1635 | Selektions-Attribute: Farbe, Lineweight, Linetype, Hatch, Plot-Sync | panel_base, massstab (sticky) |
|
||||
| `oberleiste.py` (OBERLEISTE) | 981 | Top-Bar: View/Display/Massstab-Proxy, Snaps, Window-Layout, Settings | panel_base, massstab, overrides (sticky) |
|
||||
| `massstab.py` (MASSSTAB) | 1096 | Viewport-Skala 1:N, Auto-DPI (CoreGraphics), PlotWeight | panel_base, layer_builder |
|
||||
| `overrides.py` | 797 | Engine: regelbasierte Overrides (Bedingung→Aktion), Presets cross-doc | — (Library) |
|
||||
| `overrides_panel.py` | 226 | UI auf overrides-Engine | panel_base, overrides, oberleiste (sticky) |
|
||||
| `ausschnitte.py` (AUSSCHNITTE) | 708 | Viewport-Snapshots (Kamera + Display + Layer) | panel_base, massstab |
|
||||
| `dimensionen.py` (DIMENSIONEN) | 613 | Bemaßungs-Panel (Wand-Dicken, Geschoss-Höhen, Öffnungen) | panel_base |
|
||||
| `layouts.py` (LAYOUTS) | 749 | Layout-Editor für Druckplatten | panel_base |
|
||||
| `werkzeuge.py` (WERKZEUGE) | 58 | Quick-Tools (Batch-Operationen) | panel_base |
|
||||
| `layer_builder.py` | 436 | Helper: Ebenen-Hierarchie aufbauen, Sublayer-Sync | — (Library) |
|
||||
| `startup.py` | 136 | Initialisierer: liest `dossier.project.json`, lädt Module selektiv | panel_base + Module via `__import__` |
|
||||
| `clean.py` / `clean_layers.py` / `_reset_panels.py` / `inspect_section.py` | 48-163 | Wartung / Debugging | — |
|
||||
|
||||
---
|
||||
|
||||
## 2. Tragende Patterns
|
||||
|
||||
### 2.1 Bridge-Pattern (Pflicht für jedes Panel)
|
||||
|
||||
```python
|
||||
class MyBridge(panel_base.BaseBridge):
|
||||
def __init__(self):
|
||||
panel_base.BaseBridge.__init__(self, "mymodule")
|
||||
|
||||
def _on_ready(self):
|
||||
self.send("STATE_SYNC", {...}) # WebView fertig geladen
|
||||
|
||||
def handle(self, data):
|
||||
t = data.get("type")
|
||||
if t == "ACTION": self._do_action()
|
||||
|
||||
def _bridge_factory():
|
||||
b = MyBridge()
|
||||
_install_listeners(b) # Rhino-Events registrieren
|
||||
return b
|
||||
|
||||
panel_base.register_and_open(
|
||||
"mymodule", "MY PANEL", PANEL_GUID_STR,
|
||||
_bridge_factory,
|
||||
icon_spec=("foundation", "#5fa896"), # Material-Icon-Name + Petrol
|
||||
min_size=(400, 300),
|
||||
)
|
||||
```
|
||||
|
||||
### 2.2 React ↔ Python Kommunikation
|
||||
|
||||
- **React → Python**: `document.title = "RHINOMSG::{json}"` — gepollt im Idle-Handler in `panel_base`.
|
||||
- **Python → React**: `bridge.send(type, payload)` → `webview.ExecuteScript("window.onRhinoMessage(...)")`
|
||||
- **Chunking**: Messages > 200 KB werden in `panel_base.handle_raw` automatisch gesplittet + reassembliert. Subklassen kümmern sich nicht drum.
|
||||
|
||||
### 2.3 Sticky-Storage (Cross-Module-State)
|
||||
|
||||
Konventionen für Keys:
|
||||
|
||||
- `"{modul}_bridge"` — Bridge-Instanz (in `_bridge_factory` registriert)
|
||||
- `"{modul}_listeners"` — Bool-Flag: Listener bereits registriert? (verhindert Doppel-Hook)
|
||||
- `"_dossier_*"` — globale States (z.B. `_dossier_joints_cache`, `_dossier_timing_enabled`, `_dossier_layout_applied`)
|
||||
- `"{modul}_*_cache"` — Modul-Cache (z.B. `_JOINTS_CACHE_KEY` in elemente)
|
||||
|
||||
### 2.4 Listener-Hookup (Idempotent)
|
||||
|
||||
```python
|
||||
def _on_idle(s, e):
|
||||
b = sc.sticky.get("mymodule_bridge")
|
||||
if b is not None: # IMMER None-Check
|
||||
try: b._send_state()
|
||||
except Exception: pass
|
||||
|
||||
def _install_listeners(bridge):
|
||||
flag = "mymodule_listeners"
|
||||
sc.sticky["mymodule_bridge"] = bridge
|
||||
if sc.sticky.get(flag): return # Schon registriert
|
||||
Rhino.RhinoApp.Idle += _on_idle
|
||||
Rhino.RhinoDoc.ActiveDocumentChanged += _on_view_change
|
||||
sc.sticky[flag] = True
|
||||
```
|
||||
|
||||
### 2.5 Cache-Pattern
|
||||
|
||||
- **Joint-Cache** (`elemente.py: _JOINTS_CACHE_KEY`): pro Geschoss; invalidiert bei Add/Delete/Replace.
|
||||
- **Material-Cache** (`elemente.py`): Hex→MaterialIndex; stale-Check beim Lesen.
|
||||
- **Hatch-Curve-Link** (`gestaltung.py`): UUID→Hatch in Sticky, weil Rhino UserStrings bei Move/Replace teils wegwischt.
|
||||
- **Display-Modes-Cache** (`oberleiste.py`): einmalig gelesen, Sticky-gecacht.
|
||||
- **Pending-Hatch TTL** (`gestaltung.py`): 3 s Fenster nach Drag/Move, in dem Hatch-Metadaten wiederherstellbar sind.
|
||||
|
||||
### 2.6 Settings-File
|
||||
|
||||
Pfad-Hierarchie:
|
||||
|
||||
1. **Primär** (Launcher schreibt): `~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json`
|
||||
2. **Legacy-Fallback** (read-only): `~/Library/Application Support/RhinoPanel/dossier_settings.json`
|
||||
|
||||
Bekannte Keys: `windowLayout`, `autoApplyLayout`, `pendingApplyLayout`, `rhinoApp`, `templatePath`.
|
||||
|
||||
Normalisierung: Legacy `defaultLayout` → `windowLayout` in `_settings_load`.
|
||||
|
||||
### 2.7 Window-Layouts auf Mac (XML, nicht .rwl!)
|
||||
|
||||
- Speicherort: `~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces/<GUID>.xml`
|
||||
- Display-Name aus `<RhinoUI name="...">` Attribut.
|
||||
- Apply: Reflection über `Rhino.UI.WindowLayout.*`, Fallback `_-SetActiveLayout "Name" _Enter`.
|
||||
- Live-Apply aus dem Launcher: setzt `pendingApplyLayout` im Settings-JSON; `oberleiste.tick_idle()` pollt + clearet.
|
||||
|
||||
---
|
||||
|
||||
## 3. Cross-Module-Pfade (Sticky-Bus)
|
||||
|
||||
| Sender → Empfänger | Trigger | Effekt |
|
||||
|---|---|---|
|
||||
| `rhinopanel` → `elemente` | Apply von Ebenen-Struktur (Höhen/OKFF) | `elemente_bridge._regenerate_all()` regeneriert Wände/Decken |
|
||||
| `elemente` → `rhinopanel` | Wand/Decken-Delete | `ebenen_bridge_ref._send_state()` (Fallback-Chain) |
|
||||
| `oberleiste` → `overrides` | Preset-Auswahl in Topbar | `overrides_bridge._send_state()` |
|
||||
| `massstab` ↔ `ausschnitte` | Viewport-/Zoom-Wechsel | Bi-direktional Skala lesen/setzen |
|
||||
| `gestaltung` ↔ `rhinopanel` | Hatch-Pattern auf Selektion | Pattern+Scale+Rotation-Signature vergleichen |
|
||||
|
||||
**Risiko**: Sticky-Reads ohne `is not None`-Check sind verstreut (vor allem in `oberleiste.py` an einigen Stellen). Bei Refactor immer Schutz einbauen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Bekannte Schwachstellen
|
||||
|
||||
### 4.1 `elemente.py` Monolith (7244 LOC)
|
||||
Enthält Wand-Achse+Volumen, Wand-Miter/T-Junction, Decken (Brep-Extrusion), Öffnungen (Fenster/Türen mit Rahmen+Sims+Flügel), Treppen (gerade/L/Wendel), Tragwerk (Stütze/Träger/I-Profil), Räume (SIA-416 Stempel + Farben). Vorschlag für späteren Refactor: Split in `wand.py`, `decke.py`, `oeffnung.py`, `treppe.py`, `tragwerk.py`, `raum.py` + Shared-Utils-Modul (`_read_meta`, `_geschoss_lookup`, `_active_geschoss_id`). Aktuell **nicht kritisch**, aber bremst Navigation und Tests.
|
||||
|
||||
### 4.2 Duplizierter Code
|
||||
- `_color_to_hex` / `_hex_to_color` in 3 Modulen (gestaltung, panel_base, layer_builder)
|
||||
- Geschoss-Lookup-Helper in elemente.py (gehören in layer_builder)
|
||||
- Layer-Hierarchie-Aufbau split zwischen layer_builder.py und rhinopanel.py
|
||||
|
||||
### 4.3 Cache-Stale-Risiken
|
||||
- **Joint-Cache** invalidiert bei Add/Delete/Replace — Undo/Redo wird **nicht** abgefangen
|
||||
- **Material-Cache** erkennt erst beim Zugriff dass ein Index ungültig ist; bei Material-Delete in Rhino bleibt Cache bis Restart stale
|
||||
- Workaround: `clean.py` für manuellen Cache-Clear, aber nicht automatisch
|
||||
|
||||
### 4.4 None-Check-Lücken in Sticky-Reads
|
||||
Beispiel `oberleiste.py:733` (Pfad variiert):
|
||||
```python
|
||||
b = sc.sticky.get("overrides_bridge") # kann None sein
|
||||
b._send_state() # → AttributeError wenn None
|
||||
```
|
||||
Beim nächsten Touch dieser Stellen: `if b is not None:` einziehen.
|
||||
|
||||
### 4.5 `ebenen_bridge_ref` Fallback-Chain
|
||||
Drei Lookup-Namen aus historischen Gründen (`ebenen_bridge_ref` → `ebenen_bridge` → `rhinopanel_bridge`). Bei nächstem Touch konsolidieren.
|
||||
|
||||
### 4.6 Doppel-Listener-Risiko
|
||||
Wenn zwei Module gleichzeitig laden und beide einen globalen Idle-Handler registrieren — Flag-Schutz pro Modul gut, aber kein zentrales Lock. Bisher in Praxis kein Problem.
|
||||
|
||||
---
|
||||
|
||||
## 5. Was die Architektur richtig macht (nicht anfassen ohne Grund)
|
||||
|
||||
1. **BaseBridge-Abstraktion** ist sauber: alle Bridges folgen demselben Lifecycle, WebView-Integration ist transparent.
|
||||
2. **Chunk-Handling** für Large Messages (>200 KB) in `panel_base.handle_raw` — elegant, Subklassen merken nichts.
|
||||
3. **Migration-Strategy** (`traite_` → `pause_` → `dossier_` Sticky-Prefixes): idempotent, per-Doc-Flag verhindert Mehrfach-Lauf.
|
||||
4. **Icon-System**: Multi-Fallback PNG → SVG → Material-Font → Buchstabe; gecacht.
|
||||
5. **Selective Module-Loading** über `startup.py` + `dossier.project.json`: ein Projekt zieht nur die benötigten Module.
|
||||
6. **DPI-Auto-Detection** via CoreGraphics auf Mac — robuster als die meisten alternativen Ansätze.
|
||||
7. **UTF-8 Handling konsequent**: `ensure_ascii=False`, defensive int()-Casts vor Format — vermeidet Rhino-Encoder-Bugs.
|
||||
8. **Defensives Error-Handling**: Try/Except mit `[MODUL]`-Präfix-Logging in der Rhino-Konsole; Plugin bricht nicht ab bei Einzelfehlern.
|
||||
|
||||
---
|
||||
|
||||
## 6. Launcher-Anbindung (Tauri)
|
||||
|
||||
Der **Dossier-Launcher** (`launcher/`) ist eine separate Tauri-App:
|
||||
- Verwaltet Projekt-Liste + Settings + Updates (auto via `tauri-plugin-updater`)
|
||||
- Schreibt `dossier_settings.json` in den oben (§2.6) genannten Primär-Pfad
|
||||
- Live-Push an laufende Rhino-Session: `pendingApplyLayout`-Key in Settings, `oberleiste.tick_idle()` pollt + clearet
|
||||
- System-Tray mit Quick-Open der letzten 5 Projekte (`refresh_tray_menu`-Command nach jedem Recent-Update)
|
||||
|
||||
Rhino kann ohne Launcher laufen; Launcher kann ohne Rhino laufen. IPC ist bewusst dateibasiert, kein Socket.
|
||||
|
||||
---
|
||||
|
||||
## 7. Wenn du was änderst
|
||||
|
||||
1. **Lies dieses Dokument** + den `## tragenden Patterns`-Block der CLAUDE.md
|
||||
2. **Halt dich an die Naming-Konventionen** (Sticky-Keys, Bridge-Factory)
|
||||
3. **Bei Sticky-Reads: `is not None`-Check** (siehe §4.4)
|
||||
4. **Cache invalidieren wenn dein Code Source-Daten ändert** (siehe §2.5)
|
||||
5. **Dieses Dokument up-to-date halten** wenn sich Patterns/Schwachstellen ändern
|
||||
@@ -1,160 +1,147 @@
|
||||
# RhinoPanel — Projektdokumentation für Claude
|
||||
# Dossier — Projekt-Anweisungen für Claude
|
||||
|
||||
## Was ist das?
|
||||
## Was das ist
|
||||
|
||||
Ein React-Plugin für Rhino 8 (Mac) das als schwebendes Fenster läuft und Architektur-Workflows aus Vectorworks/ArchiCAD nachbildet. Die React-UI wird in Rhinos Eto.Forms WebView über `LoadHtml` (inline) eingebettet.
|
||||
**Dossier** ist ein Rhino 8 Plugin (Mac) mit React-WebView-Panels für
|
||||
architektonische Workflows (Wände, Decken, Öffnungen, Räume, SIA-416,
|
||||
Plan-Layouts). Teil der OpenStudio-Suite. Schwester-App: Rapport.
|
||||
|
||||
## Kommunikation React ↔ Python
|
||||
**Dossier-Launcher** ist eine separate Tauri-App (`launcher/`), die Projekte
|
||||
verwaltet, Settings hält (auch für den Plugin), Auto-Updates liefert und im
|
||||
System-Tray lebt.
|
||||
|
||||
**React → Python:** `document.title = "RHINOMSG::{json}"` (queue-basiert, 80ms delay)
|
||||
**Python → React:** `webview.ExecuteScript("window.onRhinoMessage({...})")`
|
||||
## Runtime: Python 3.9 CPython (verifiziert 2026-05-17)
|
||||
|
||||
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
|
||||
Dieser Ordner `DOSSIER/` läuft mit Rhinos **neuem Python-3-Engine** (CPython
|
||||
3.9.10 via Script Editor). Der Vorgänger `rhino-panel/` bleibt **frozen** als
|
||||
IPy-2.7-Referenz.
|
||||
|
||||
## Datenmodell
|
||||
**Wie geladen wird (WICHTIG):**
|
||||
- ✅ **`_ScriptEditor`** → Datei öffnen + Run-Button → CPython 3.9 (Shebang
|
||||
`#! python3` wird respektiert). Das ist der **funktionierende Pfad**.
|
||||
- ✅ **Rhino Options → General → Command Lists → „Run these commands every
|
||||
time a model is opened"** → Form: `_-RunPythonScript "/voller/pfad.py"`
|
||||
(mit Dash + Quotes). Persistiert in `Options/General/StartupCommands` der
|
||||
`settings-Scheme__Default.xml`. Lädt das Skript bei jedem Rhino-Start
|
||||
silent, ohne File-Dialog. Trotz Dash bleibt Shebang `#! python 3`
|
||||
wirksam → CPython 3.9 (verifiziert 2026-05-17 Mac Rhino 8). Der Launcher
|
||||
trägt diesen Eintrag automatisch ein.
|
||||
- ⚠️ `_-RunPythonScript "path.py"` mit Dash in der **interaktiven Command-
|
||||
Line** → IronPython 2.7 (Legacy). NICHT benutzen für DOSSIER, sonst
|
||||
crasht die SectionStyle-Logik etc.
|
||||
- ⚠️ `_RunPythonScript path.py` ohne Dash — in der Command-Line OK (Py3
|
||||
via Shebang, ohne Quotes, in einer Zeile). Im StartupCommands-Feld
|
||||
öffnet diese Form aber einen File-Dialog statt zu laufen.
|
||||
|
||||
```json
|
||||
[
|
||||
{"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"}
|
||||
]
|
||||
**Migration-Stand:**
|
||||
- ✅ Repo kopiert nach `DOSSIER/`, alle Pfade umgestellt
|
||||
- ✅ Shebangs aller Files auf `#! python3` (ohne Space) — Format das Rhino erkennt
|
||||
- ✅ Code war bereits Py3-syntax-kompatibel (kein xrange/iteritems/unicode())
|
||||
- ✅ Plugin läuft als CPython 3.9.10, alle 9 Panels registrieren
|
||||
- ✅ Reflection.Emit + Eto.Forms + Bridge funktionieren
|
||||
- ✅ **`Rhino.DocObjects.SectionStyle()` instanziierbar** + `layer.SetCustomSectionStyle()`
|
||||
verfügbar — die volle Section-Style-API ist jetzt zugänglich (Anlass der Migration)
|
||||
|
||||
## Bei Code-Arbeit — Reihenfolge
|
||||
|
||||
1. **`ARCHITECTURE.md` zuerst lesen** — Module-Map, Konventionen, Schwachstellen.
|
||||
2. **Dann das relevante Modul lesen**. Nicht raten was drin steht.
|
||||
3. **Erst danach editieren**.
|
||||
|
||||
## Anti-Over-Engineering
|
||||
|
||||
Diese Regeln sind nicht verhandelbar — Verstöße kosten Zeit beim Aufräumen.
|
||||
|
||||
- **Keine Abstraktionen einführen, die ein konkretes Problem lösen.**
|
||||
Drei ähnliche Zeilen sind besser als eine Hilfsfunktion mit drei Aufrufstellen.
|
||||
- **Kein „aufräumen drumherum".** Ein Bugfix bleibt ein Bugfix.
|
||||
- **Kein Error-Handling für Szenarien, die nicht eintreten können.**
|
||||
- **Keine Feature-Flags, keine Migrations-Shims parallel zur alten Funktion.**
|
||||
- **Keine Kommentare die WAS sagen** — nur WARUM-Kommentare wenn nicht-offensichtlich.
|
||||
- **Keine erfundenen Module/Funktionen/Flags.** Erst `grep`, dann editieren.
|
||||
|
||||
## Anti-Patterns (aus echten Sessions)
|
||||
|
||||
- **`try/except: pass` als Bug-Verstecker.** Lieber `print("[MODUL] err:", ex)` und weiter.
|
||||
- **Sticky-Reads ohne `is not None`-Check.** Siehe `ARCHITECTURE.md §4.4`.
|
||||
- **„Cleveres" Refactoring von Wand-Geometrie.** `elemente.py` (7244 LOC)
|
||||
enthält BIM-Logik die in Echtbau-Projekten läuft. NIE in einem Rutsch
|
||||
modularisieren ohne expliziten Auftrag + Test-Plan.
|
||||
- **Mac vs. Windows Rhino-Pfade verwechseln.** Mac Rhino 8 speichert
|
||||
Window-Layouts als **XML in `Scheme__Default/workspaces/<GUID>.xml`**, nicht
|
||||
als `.rwl`.
|
||||
- **Python-Runtime annehmen statt prüfen.** Diagnose mit `print(sys.version)`
|
||||
— das hat uns 4 Wochen versteckte IPy2.7 gekostet.
|
||||
- **`document.title` mit Umlauten kaputt machen.** UI-Strings dürfen Umlaute;
|
||||
**Python-Backend bevorzugt `ue/oe/ae`** in Identifiern, Layer-Codes,
|
||||
UserString-Values.
|
||||
|
||||
## Python-Konventionen — POST-MIGRATION TARGET
|
||||
|
||||
Sobald die Migration durch ist, gilt:
|
||||
|
||||
- Datei-Header: `# ! python3` + `# -*- coding: utf-8 -*-` (werden dann wirksam)
|
||||
- **Aufruf in Rhino:** `_RunPythonScript "path"` (ohne Dash!) — sonst startet
|
||||
IPy 2.7. Alternative: `_-ScriptEditor` mit Code-Engine
|
||||
- `print(x)` — IMMER mit Klammern (Python-3-Style)
|
||||
- `builtins` statt `__builtin__`
|
||||
- f-strings erlaubt: `f"value: {x}"`
|
||||
- CLR: `import clr; clr.AddReference("System.Drawing"); from System.Drawing import Color`
|
||||
- UI-Strings dürfen Umlaute, Code-Identifier nicht (`tuer`, nicht `tür`)
|
||||
|
||||
## Build & Reset (Cheatsheet)
|
||||
|
||||
```bash
|
||||
# Rhino-Panels Frontend
|
||||
npm run build # im Repo-Root
|
||||
|
||||
# Launcher Frontend
|
||||
cd launcher && npm run build
|
||||
|
||||
# Launcher Backend Check
|
||||
cd launcher/src-tauri && cargo check
|
||||
|
||||
# Launcher Release (signiert, schreibt latest.json)
|
||||
cd launcher && ./scripts/release.sh
|
||||
|
||||
# Python-Syntax-Check (kein Rhino nötig)
|
||||
python3 -c "import ast; ast.parse(open('rhino/elemente.py').read())"
|
||||
|
||||
# Runtime-Verify in Rhino (welcher Python-Engine läuft wirklich?)
|
||||
_RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/startup.py
|
||||
# Erste Zeile sollte zeigen: [STARTUP] Python: 3.x ... nach Migration
|
||||
```
|
||||
|
||||
Gespeichert in `doc.Strings["rhinopanel_ebenen"]` (JSON). OKFF wird nur für `type: "grundriss"` berechnet (kumulativ). Schnitt/Ansicht haben keine Höhenparameter.
|
||||
**Plugin reset in Rhino** (nach Python-Änderungen):
|
||||
|
||||
## Rhino Layer-Hierarchie
|
||||
|
||||
```
|
||||
10_GRUNDRISSE
|
||||
└── EG
|
||||
├── 01_WAND (schwarz, lw 0.50)
|
||||
├── 02_TUER_FENSTER (blau, lw 0.25)
|
||||
├── 03_MOEBEL (grau, lw 0.13)
|
||||
├── 04_TEXT (hellgrau, lw 0.13)
|
||||
├── 05_TREPPEN (gold, lw 0.35)
|
||||
└── 06_3D_VOLUMEN (lila, lw 0.25)
|
||||
└── 1OG (gleiche Sublayer)
|
||||
20_SCHNITTE
|
||||
└── Schnitt A-A
|
||||
├── 21_PROFIL (rot, lw 0.70)
|
||||
├── 22_WAND (orange, lw 0.25)
|
||||
└── 23_TEXT (hellgrau, lw 0.13)
|
||||
30_ANSICHTEN
|
||||
└── Nordansicht
|
||||
├── 31_FASSADE (türkis, lw 0.35)
|
||||
└── 32_TEXT (hellgrau, lw 0.13)
|
||||
00_RASTER, 01_VERMESSUNG, 40_SITUATION, 90_REFERENZEN, 99_KONSTRUKTION
|
||||
```
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
```
|
||||
rhino-panel/
|
||||
├── src/
|
||||
│ ├── App.jsx # Hauptkomponente, State-Management
|
||||
│ ├── main.jsx # Entry point, window.onerror handler
|
||||
│ ├── index.css # Dark theme, CSS-Variablen (Swiss minimal)
|
||||
│ ├── lib/
|
||||
│ │ └── rhinoBridge.js # Kommunikation React↔Python
|
||||
│ └── components/
|
||||
│ ├── EbenenManager.jsx # Zeichnungsebenen-Liste (GR/SC/AN Badges)
|
||||
│ ├── EbenenDialog.jsx # Dialog zum Bearbeiten der Ebenen
|
||||
│ ├── LayerPanel.jsx # Layer-Sichtbarkeit/Sperre togglen
|
||||
│ ├── BottomBar.jsx # "Auf Rhino anwenden" Button
|
||||
│ └── Section.jsx # Ausklappbarer Abschnitt
|
||||
├── rhino/
|
||||
│ ├── rhinopanel.py # Panel starten, Bridge, LoadHtml-Inline
|
||||
│ ├── layer_builder.py # Rhino-Layer erstellen/aktualisieren
|
||||
│ └── INSTALL.md # Setup-Anleitung inkl. GH
|
||||
├── dist/ # Gebaute App (npm run build)
|
||||
└── vite.config.js # base: './' wichtig für file:// URLs
|
||||
```
|
||||
|
||||
**Alte Dateien (nicht mehr aktiv, können gelöscht werden):**
|
||||
- `src/components/GeschossManager.jsx` — ersetzt durch EbenenManager
|
||||
- `src/components/GeschossDialog.jsx` — ersetzt durch EbenenDialog
|
||||
|
||||
## Kritische technische Details
|
||||
|
||||
### Warum LoadHtml statt file:// URL
|
||||
Rhinos WKWebView blockiert `<script type="module">` bei `file://` URLs (CORS/module restrictions). Lösung: Python liest `dist/index.html`, inliniert CSS und JS, lädt via `webview.LoadHtml(html)`. JS wird in `DOMContentLoaded` eingewickelt damit `#root` existiert wenn React rendert.
|
||||
|
||||
### Warum Idle-Event für URL-Loading
|
||||
`LoadHtml` in `__init__` läuft bevor der WebView in einem Fenster ist → React kann nicht in DOM rendern. Fix: `Rhino.RhinoApp.Idle` Event wartet bis Fenster sichtbar ist.
|
||||
|
||||
### Warum kein Docking
|
||||
`RegisterPanel` schlägt fehl: `"constructor must accept uint, RhinoDoc or no params"`. IronPython `*args` befriedigt Rhinos .NET-Reflection nicht. Panel läuft daher als schwebendes `forms.Form`.
|
||||
|
||||
### IronPython 2 Limitierungen
|
||||
- `# -*- coding: utf-8 -*-` Header in allen .py Dateien
|
||||
- Keine Umlaute in Strings (ue/oe/ae statt ü/ö/ä)
|
||||
- `e.Title` statt `webview.Title` im DocumentTitleChanged Handler
|
||||
|
||||
## Grasshopper-Anbindung
|
||||
|
||||
Wenn "Anwenden" gedrückt wird:
|
||||
1. `layer_builder.build_layers()` erstellt/aktualisiert Rhino-Layer
|
||||
2. JSON wird in `doc.Strings["rhinopanel_ebenen"]` gespeichert
|
||||
3. `canvas.Document.NewSolution(True)` triggert GH-Neuberechnung
|
||||
|
||||
**GH Python-Komponente (eine Komponente, Output `a`):**
|
||||
```python
|
||||
import json, Rhino, Rhino.Geometry as rg
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
raw = doc.Strings.GetValue("rhinopanel_ebenen")
|
||||
ebenen = json.loads(raw) if raw else []
|
||||
a = []
|
||||
for e in ebenen:
|
||||
if e.get("type") != "grundriss": continue
|
||||
idx = doc.Layers.FindByFullPath("10_GRUNDRISSE::{}::01_WAND".format(e["name"]), -1)
|
||||
if idx < 0: continue
|
||||
for obj in doc.Objects.FindByLayer(doc.Layers[idx]):
|
||||
crv = obj.Geometry
|
||||
if not isinstance(crv, rg.Curve): continue
|
||||
c = crv.DuplicateCurve()
|
||||
c.Transform(rg.Transform.Translation(0, 0, e.get("okff", 0)))
|
||||
ext = rg.Extrusion.Create(c, e["hoehe"], c.IsClosed)
|
||||
if ext: a.append(ext.ToBrep())
|
||||
_RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_reset_panels.py
|
||||
```
|
||||
Für Live-Updates: GH `Timer` Komponente (1000ms) mit Python-Komponente verbinden.
|
||||
|
||||
## Design-System
|
||||
|
||||
Dark theme, Swiss minimal, inspiriert von rapport.kgva.ch.
|
||||
CSS-Variablen in `index.css`. Wichtigste:
|
||||
- `--bg-base: #1c1c1e` — Hintergrund
|
||||
- `--accent: #5a9e5a` — Grün (Anwenden-Button)
|
||||
- `--active: #3a5f8a` — Blau (aktive Ebene)
|
||||
- `--font: 'Inter'` — via Google Fonts
|
||||
- Petrol-Grün `--accent: #5fa896` (Hauptakzent), dunkles Petrol `#2f5d54`
|
||||
- Hintergrund `--bg: #0e1413`
|
||||
- Fonts: Krungthep/Archivo Black für „DOSSIER"-Logo, Playfair Display für
|
||||
Headings, DM Mono für Body
|
||||
- Konsistent zur Website (`/Users/karim/STUDIO/DOSSIER-WEBSITE/`)
|
||||
|
||||
Typ-Badges: GR=blau `#3a6fa8`, SC=orange `#c87050`, AN=türkis `#50c8a0`
|
||||
## Settings-Files (Pfade)
|
||||
|
||||
## Workflow Panel starten
|
||||
- **Launcher → Rhino IPC** (file-based, kein Socket):
|
||||
`~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json`
|
||||
- Legacy-Fallback (read-only): `~/Library/Application Support/RhinoPanel/dossier_settings.json`
|
||||
- Launcher-Cache: `~/Library/Application Support/ch.gabrielevarano.Dossier/recent.json`
|
||||
|
||||
```bash
|
||||
npm run build # nach Code-Änderungen
|
||||
```
|
||||
## Vorgänger-Codebase
|
||||
|
||||
In Rhino (bei Änderungen am Python-Code):
|
||||
```python
|
||||
import scriptcontext as sc
|
||||
sc.sticky["rhinopanel_registered"] = False
|
||||
sc.sticky["rhinopanel_form"] = None
|
||||
```
|
||||
→ `_RunPythonScript` → `rhinopanel.py`
|
||||
Der alte Ordner `/Users/karim/STUDIO/rhino-panel/` bleibt als **read-only
|
||||
Referenz** stehen. Wenn ein Feature dort funktioniert das hier noch nicht
|
||||
portiert ist — dort schauen, dann hier neu in Python-3-Style implementieren.
|
||||
**Nicht** einfach kopieren, sondern beim Übertragen den Migrations-Style
|
||||
anpassen.
|
||||
|
||||
## Nächste mögliche Schritte
|
||||
## Wenn unklar — fragen, nicht raten
|
||||
|
||||
- **Docking**: Rhino 8 RhinoCode (Python 3) statt IronPython verwenden — hat andere Panel-Registration API
|
||||
- **Live-Vorschau**: Rhino `ObjectAdded`/`ObjectModified` Events in rhinopanel.py für automatischen GH-Trigger ohne Anwenden-Button
|
||||
- **Schnittlinien**: Für SC-Ebenen Schnittlinie/Position als Parameter hinzufügen
|
||||
- **Layerpanel dynamisch**: LayerPanel.jsx zeigt aktuell statische Layer-Gruppen — soll dynamisch aus `ebenen`-State kommen
|
||||
- **2D-Fills**: Schraffuren/Füllungen für Wände (Vectorworks-ähnlich)
|
||||
- **Viewport-Zuweisung**: Grundrisse/Schnitte/Ansichten einem Rhino-Detailbild zuweisen
|
||||
Wenn die Aufgabe ambig ist oder Konsequenzen über die offensichtlichen
|
||||
hinausgehen: erst nachfragen.
|
||||
|
||||
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Rhino-8 Plugin für architektonisches Entwerfen mit smarten Bauteilen — Geschosse, Wände, Decken, Dächer, Öffnungen (Fenster/Türen), Treppen (gerade · L · Wendel). Teil der **OpenStudio-Suite** (mit Rapport als Schwestertool).
|
||||
|
||||
Die React-UI wird in Rhinos Eto.Forms-WebView über `LoadHtml` (inline) eingebettet — die Plugin-Logik läuft in IronPython3 in Rhino 8 (Mac).
|
||||
Die React-UI wird in Rhinos Eto.Forms-WebView über `LoadHtml` (inline) eingebettet — die Plugin-Logik läuft in **CPython 3.9** (Rhino 8 Script-Editor-Engine, Mac).
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
@@ -11,7 +11,7 @@ Die React-UI wird in Rhinos Eto.Forms-WebView über `LoadHtml` (inline) eingebet
|
||||
| Rhino | 8 (Mac · Windows untestet) |
|
||||
| Node.js | ≥ 20 (für Vite 8) |
|
||||
| npm | ≥ 10 |
|
||||
| Python | IronPython 3 (in Rhino integriert) |
|
||||
| Python | CPython 3.9 (Rhino 8 Script-Editor-Engine) |
|
||||
|
||||
Optional — für den Standalone-Launcher:
|
||||
|
||||
@@ -48,7 +48,7 @@ In Rhino 8 das Hauptpanel über `_RunPythonScript` öffnen:
|
||||
|
||||
```python
|
||||
# Hauptmenu
|
||||
_RunPythonScript "/Users/karim/STUDIO/rhino-panel/rhino/rhinopanel.py"
|
||||
_RunPythonScript "/Users/karim/STUDIO/DOSSIER/rhino/rhinopanel.py"
|
||||
```
|
||||
|
||||
Bei Änderungen am Python-Code Panels neu laden:
|
||||
@@ -105,7 +105,8 @@ for m in list(sys.modules):
|
||||
│ ├── MassstabApp.jsx Massstab/Display-Modes
|
||||
│ ├── DimensionenApp.jsx Objekt-Info (Position/Abmessungen)
|
||||
│ ├── OverridePanel.jsx Override-Regeln + Kombinationen
|
||||
│ ├── components/ EbenenManager, GeschossManager, ...
|
||||
│ ├── TextEditorApp.jsx DOSSIER-Text WYSIWYG-Editor (Rich-Text via RTF)
|
||||
│ ├── components/ EbenenManager, GeschossManager, BarControls (shared Pill-UI), ...
|
||||
│ └── lib/rhinoBridge.js React↔Python Bridge
|
||||
├── rhino/ Backend (IronPython 3)
|
||||
│ ├── rhinopanel.py Haupt-Entry, Bridge-Pattern
|
||||
@@ -118,6 +119,8 @@ for m in list(sys.modules):
|
||||
│ ├── dimensionen.py Objekt-Info Panel
|
||||
│ ├── gestaltung.py Gestaltung (Override-Editor)
|
||||
│ ├── werkzeuge.py Werkzeug-Sammlung
|
||||
│ ├── text_editor.py DOSSIER-Text Backend (Frame-Pick + Rich-Text-RTF)
|
||||
│ ├── text_create.py Text-Styles, Font-Apply, Selection-Settings
|
||||
│ └── oberleiste.py Top-Menue (verbindet alle Panels)
|
||||
├── launcher/ Tauri-2 Standalone-Launcher (optional)
|
||||
├── dist/ Gebaute React-App (npm run build)
|
||||
@@ -128,9 +131,10 @@ for m in list(sys.modules):
|
||||
|
||||
## Bekannte Limitierungen
|
||||
|
||||
- IronPython3-spezifisch: keine Umlaute in Source-Strings (`ue/oe/ae` statt `ü/ö/ä`); UTF-8-Header-Kommentar in allen `.py`-Files.
|
||||
- **Python-Identifier ohne Umlaute** (`ue/oe/ae` statt `ü/ö/ä`) — UI-Strings dürfen Umlaute, Code-Bezeichner / Layer-Codes / UserString-Keys nicht. Konvention seit der Py3-Migration.
|
||||
- **Kein Docking** der Panels (Rhinos `RegisterPanel` schlägt fehl: `"constructor must accept uint, RhinoDoc or no params"`). Panels laufen daher als schwebende `forms.Form`-Fenster.
|
||||
- **`LoadHtml`-inline** statt `file://`-URL — Rhinos WKWebView blockiert sonst `<script type="module">` durch CORS-Restrictions.
|
||||
- **TextEntity-RTF**: Rhinos eingebauter Parser unterstützt nur `\b \i \ul \strike \fN \tab {}` plus Newline-via-`\par`-zwischen-Groups. **Kein `\fs`** (= eine TextEntity hat global eine Schriftgröße, keine per-Segment-Sizes). Newlines/Replace-Quirks siehe `_runs_to_rtf` in `rhino/text_editor.py`.
|
||||
|
||||
## Lizenz
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# DOSSIER C# Plugin — Build & Install
|
||||
|
||||
Das Plugin (`.rhp`) bootstrappt beim Rhino-Start die Python-Module und
|
||||
registriert native Commands (`dWall`, `dDoor`, `dSlab`, …).
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
```bash
|
||||
brew install dotnet@7
|
||||
```
|
||||
|
||||
Oder direkt von Microsoft: https://dotnet.microsoft.com/download/dotnet/7.0
|
||||
|
||||
RhinoCommon wird beim ersten Build automatisch via NuGet geladen.
|
||||
|
||||
## Repo-Pfad setzen (nach Neuinstallation wichtig)
|
||||
|
||||
Das Plugin sucht das Repo in dieser Reihenfolge:
|
||||
|
||||
1. Env-Var `DOSSIER_HOME`
|
||||
2. Datei `~/.dossier_home` (eine Zeile: absoluter Pfad zum Repo-Root)
|
||||
3. Hardcoded Fallback `/Users/karim/STUDIO/DOSSIER`
|
||||
|
||||
Einfachste Variante — einmalig nach dem Klonen:
|
||||
|
||||
```bash
|
||||
echo "/Users/karim/PROJECTS/DOSSIER" > ~/.dossier_home
|
||||
```
|
||||
|
||||
Ohne das findet das Plugin `rhino/startup.py` nicht und bootet nicht.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd csharp/DOSSIER
|
||||
./build.sh # Release → bin/Release/net7.0/DOSSIER.rhp
|
||||
./build.sh debug # Debug-Build mit Symbols
|
||||
./build.sh clean # bin/ + obj/ löschen
|
||||
./build.sh install # Build + yak install in Rhino-User-Plugin-Pfad
|
||||
```
|
||||
|
||||
## Installation in Rhino (einmalig nach Build)
|
||||
|
||||
Mac Rhino 8 unterstützt kein Drag & Drop für `.rhp`-Dateien.
|
||||
|
||||
1. Rhino 8 öffnen
|
||||
2. Befehl: `PluginManager`
|
||||
3. Button **Install…** → `csharp/DOSSIER/bin/Release/net7.0/DOSSIER.rhp`
|
||||
4. Rhino neu starten
|
||||
|
||||
Der Pfad bleibt in Rhinos Settings-XML registriert. Bei späteren Builds
|
||||
einfach wieder in denselben Output-Pfad bauen — Rhino lädt den neuen Stand
|
||||
automatisch beim nächsten Start.
|
||||
|
||||
## Startup-Eintrag (Python-Bootstrap)
|
||||
|
||||
Der Launcher trägt den Startup-Eintrag automatisch ein. Für manuelle
|
||||
Dev-Setups ohne Launcher:
|
||||
|
||||
Rhino → Options → General → „Run these commands every time a model is opened":
|
||||
|
||||
```
|
||||
_-RunPythonScript "/Users/karim/PROJECTS/DOSSIER/rhino/startup.py"
|
||||
```
|
||||
|
||||
(Mit Dash, mit Quotes, voller Pfad — siehe CLAUDE.md für Details warum.)
|
||||
|
||||
## Nach Neuinstallation Mac — Checkliste
|
||||
|
||||
- [ ] Repo klonen: `git clone https://git.kgva.ch/karim/DOSSIER.git`
|
||||
- [ ] `echo "/Users/karim/PROJECTS/DOSSIER" > ~/.dossier_home`
|
||||
- [ ] `brew install dotnet@7 node`
|
||||
- [ ] `cd DOSSIER && npm install && npm run build`
|
||||
- [ ] `cd csharp/DOSSIER && ./build.sh install`
|
||||
- [ ] Rhino öffnen → PluginManager → Install → `.rhp` registrieren
|
||||
- [ ] Rhino neu starten → DOSSIER bootstrappt (Panels + Commands)
|
||||
- [ ] `~/Library/Application Support/RhinoPanel/override_presets.json` von Backup zurückkopieren
|
||||
@@ -0,0 +1,7 @@
|
||||
bin/
|
||||
obj/
|
||||
dist/
|
||||
*.user
|
||||
*.suo
|
||||
.vs/
|
||||
.idea/
|
||||
@@ -0,0 +1,94 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
//
|
||||
// BIM-Commands: jeweils Wrapper auf das Python-Script in rhino/aliases/cmd/.
|
||||
// Naming-Convention: d-Prefix + englischer BIM-Begriff (VisualARQ-Stil).
|
||||
// Klassen-Guids sind frei generiert (uuidgen) — wichtig nur dass sie
|
||||
// stabil bleiben, damit Rhino sie ueber Sessions wiedererkennt.
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace DOSSIER.Cmd;
|
||||
|
||||
[Guid("9A87B609-719F-468B-AF2A-6E59A9B61062")]
|
||||
public class DWall : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dWall";
|
||||
protected override string ScriptRelativePath => "cmd/wand.py";
|
||||
}
|
||||
|
||||
[Guid("80278984-16B8-485B-8876-3D63806BCA58")]
|
||||
public class DDoor : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dDoor";
|
||||
protected override string ScriptRelativePath => "cmd/tuer.py";
|
||||
}
|
||||
|
||||
[Guid("20D22047-03FA-4CF3-ACF3-3424A109BD91")]
|
||||
public class DWindow : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dWindow";
|
||||
protected override string ScriptRelativePath => "cmd/fenster.py";
|
||||
}
|
||||
|
||||
[Guid("536641ED-93D4-4D49-A028-9F2C4EEE2A24")]
|
||||
public class DSlab : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dSlab";
|
||||
protected override string ScriptRelativePath => "cmd/decke.py";
|
||||
}
|
||||
|
||||
[Guid("CF196C6A-EEAE-478C-8EB0-C69B6F7B9942")]
|
||||
public class DStair : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dStair";
|
||||
protected override string ScriptRelativePath => "cmd/treppe.py";
|
||||
}
|
||||
|
||||
[Guid("B4A19B8B-4056-428B-BA2E-DF69A2A8DA9A")]
|
||||
public class DColumn : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dColumn";
|
||||
protected override string ScriptRelativePath => "cmd/stuetze.py";
|
||||
}
|
||||
|
||||
[Guid("CCDF2D03-1FBD-4BC3-A06E-6D3FEEE575AB")]
|
||||
public class DBeam : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dBeam";
|
||||
protected override string ScriptRelativePath => "cmd/traeger.py";
|
||||
}
|
||||
|
||||
[Guid("6ADF4344-0C05-48D1-BB2A-B330E3057CE4")]
|
||||
public class DRoom : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dRoom";
|
||||
protected override string ScriptRelativePath => "cmd/raum.py";
|
||||
}
|
||||
|
||||
[Guid("A9D471FD-CB75-4C4E-8236-3C8B9A491266")]
|
||||
public class DSymbol : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dSymbol";
|
||||
protected override string ScriptRelativePath => "cmd/symbol.py";
|
||||
}
|
||||
|
||||
[Guid("5388E3A7-B40E-40CE-B958-4A294B1E9F4F")]
|
||||
public class DTag : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dTag";
|
||||
protected override string ScriptRelativePath => "cmd/stempel.py";
|
||||
}
|
||||
|
||||
[Guid("F0A5E3B0-F77E-4316-B521-294979F1E9CA")]
|
||||
public class DRoof : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dRoof";
|
||||
protected override string ScriptRelativePath => "cmd/dach.py";
|
||||
}
|
||||
|
||||
[Guid("404E4389-F8BF-4BAE-A972-60EADB33941C")]
|
||||
public class DVoid : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dVoid";
|
||||
protected override string ScriptRelativePath => "cmd/aussparung.py";
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace DOSSIER.Cmd;
|
||||
|
||||
[Guid("07F23908-EF40-4A98-A550-C8D8A1F80A7F")]
|
||||
public class DJoin : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dJoin";
|
||||
protected override string ScriptRelativePath => "cmd/smart_join.py";
|
||||
}
|
||||
|
||||
[Guid("69DBE84C-5E44-4155-84CB-D67329B64830")]
|
||||
public class DSplit : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dSplit";
|
||||
protected override string ScriptRelativePath => "cmd/smart_split.py";
|
||||
}
|
||||
|
||||
[Guid("38E80D26-5270-45C6-B5F3-2E2179545C47")]
|
||||
public class DPipette : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dPipette";
|
||||
protected override string ScriptRelativePath => "cmd/pipette.py";
|
||||
}
|
||||
|
||||
[Guid("F2C8B5A1-9D4E-4F73-B2C6-1A8E7D3F5C42")]
|
||||
public class DSection : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dSection";
|
||||
protected override string ScriptRelativePath => "cmd/section.py";
|
||||
}
|
||||
|
||||
[Guid("66647D04-F324-459F-82B9-0FD82307FA93")]
|
||||
public class DKeys : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dKeys";
|
||||
protected override string ScriptRelativePath => "cmd/dkeys.py";
|
||||
}
|
||||
|
||||
[Guid("93406D93-E9AC-424D-BFBD-3B7A542A85A7")]
|
||||
public class DWelcome : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dWelcome";
|
||||
protected override string ScriptRelativePath => "cmd/dwelcome.py";
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace DOSSIER.Cmd;
|
||||
|
||||
[Guid("4498B184-E064-4049-8B43-873721ECEE71")]
|
||||
public class DPlan : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dPlan";
|
||||
protected override string ScriptRelativePath => "view/plan.py";
|
||||
}
|
||||
|
||||
[Guid("D6089B7C-C513-4A39-A62B-5A5E91764A18")]
|
||||
public class D3D : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "d3D";
|
||||
protected override string ScriptRelativePath => "view/persp3d.py";
|
||||
}
|
||||
|
||||
[Guid("BA89B2DE-2301-4E0D-8542-3BDF393BF7A7")]
|
||||
public class DMaterial : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dMaterial";
|
||||
protected override string ScriptRelativePath => "view/material.py";
|
||||
}
|
||||
|
||||
[Guid("A802824C-BC9B-405B-88A4-77125AA7D5A9")]
|
||||
public class DLevelUp : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dLevelUp";
|
||||
protected override string ScriptRelativePath => "view/geschoss_up.py";
|
||||
}
|
||||
|
||||
[Guid("A034FF6F-0BCC-48D7-9AC9-8447D5718D32")]
|
||||
public class DLevelDown : DossierPythonCommand
|
||||
{
|
||||
public override string EnglishName => "dLevelDown";
|
||||
protected override string ScriptRelativePath => "view/geschoss_down.py";
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<AssemblyName>DOSSIER</AssemblyName>
|
||||
<RootNamespace>DOSSIER</RootNamespace>
|
||||
<Version>0.2.0</Version>
|
||||
<Title>DOSSIER</Title>
|
||||
<Company>Karim Gabriele Varano</Company>
|
||||
<Description>DOSSIER — Architektur-Studio-Plugin fuer Rhino 8. Bootstrappt beim Plugin-Load die Python-Module (Panels, Aliases, View-Modes, Welcome) und registriert native Commands (dWall, dDoor, dStair, ...) als saubere Wrapper auf die jeweiligen Python-Scripts.</Description>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- Rhino-Plugin-Output: .rhp statt .dll -->
|
||||
<TargetExt>.rhp</TargetExt>
|
||||
<NoWarn>NU1701;NETSDK1086</NoWarn>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
|
||||
<!-- Kein Konflikt mit Rhinos Eto/WPF -->
|
||||
<UseWindowsForms>false</UseWindowsForms>
|
||||
<UseWpf>false</UseWpf>
|
||||
|
||||
<!-- Plugin-Metadaten (sichtbar im _PluginManager) -->
|
||||
<AssemblyTitle>DOSSIER</AssemblyTitle>
|
||||
<Copyright>Copyright (C) 2026 Karim Gabriele Varano. AGPL-3.0-or-later.</Copyright>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RhinoCommon" Version="8.0.23304.9001" IncludeAssets="compile;build" />
|
||||
<!-- Rhino-CPython3-Runtime — direkt aus dem App-Bundle linken.
|
||||
Erlaubt RhinoCode-API ohne den Umweg ueber _-RunPythonScript-Command. -->
|
||||
<Reference Include="Rhino.Runtime.Code">
|
||||
<HintPath>/Applications/Rhino 8.app/Contents/Frameworks/RhCore.framework/Versions/A/Resources/Rhino.Runtime.Code.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Plugin-Guid als Assembly-Attribut (Rhino registriert Plugin via dieser ID) -->
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
|
||||
<_Parameter1>Rhino.PlugIns.DescriptionType.Address</_Parameter1>
|
||||
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
|
||||
<_Parameter2>-</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
|
||||
<_Parameter1>Rhino.PlugIns.DescriptionType.Email</_Parameter1>
|
||||
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
|
||||
<_Parameter2>karim@gabrielevarano.ch</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
|
||||
<_Parameter1>Rhino.PlugIns.DescriptionType.Organization</_Parameter1>
|
||||
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
|
||||
<_Parameter2>Karim Gabriele Varano</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
|
||||
<_Parameter1>Rhino.PlugIns.DescriptionType.WebSite</_Parameter1>
|
||||
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
|
||||
<_Parameter2>https://github.com/karimgvarano/DOSSIER</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.InteropServices.GuidAttribute">
|
||||
<_Parameter1>e8a4d2c1-6b3f-4e89-9c5a-1d2e3f4a5b6c</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace DOSSIER;
|
||||
|
||||
/// <summary>
|
||||
/// Locator-Pfade fuer das DOSSIER-Repo. Reihenfolge:
|
||||
/// 1. Env-Var DOSSIER_HOME
|
||||
/// 2. File ~/.dossier_home (eine Zeile mit dem Pfad)
|
||||
/// 3. Hardcoded Fallback /Users/karim/STUDIO/DOSSIER (Dev-Setup)
|
||||
/// </summary>
|
||||
internal static class DossierPaths
|
||||
{
|
||||
private const string FallbackRoot = "/Users/karim/STUDIO/DOSSIER";
|
||||
private const string MarkerFile = ".dossier_home";
|
||||
|
||||
private static string? _cachedRoot;
|
||||
|
||||
public static string? Root
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_cachedRoot is not null) return _cachedRoot;
|
||||
_cachedRoot = ResolveRoot();
|
||||
return _cachedRoot;
|
||||
}
|
||||
}
|
||||
|
||||
public static string RhinoDir => Path.Combine(Root ?? FallbackRoot, "rhino");
|
||||
|
||||
public static string AliasDir => Path.Combine(RhinoDir, "aliases");
|
||||
|
||||
public static string CmdDir => Path.Combine(AliasDir, "cmd");
|
||||
|
||||
public static string ViewDir => Path.Combine(AliasDir, "view");
|
||||
|
||||
public static string StartupPy => Path.Combine(RhinoDir, "startup.py");
|
||||
|
||||
private static string? ResolveRoot()
|
||||
{
|
||||
var env = Environment.GetEnvironmentVariable("DOSSIER_HOME");
|
||||
if (!string.IsNullOrEmpty(env) && Directory.Exists(env)) return env;
|
||||
|
||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var marker = Path.Combine(home, MarkerFile);
|
||||
if (File.Exists(marker))
|
||||
{
|
||||
try
|
||||
{
|
||||
var p = File.ReadAllText(marker).Trim();
|
||||
if (!string.IsNullOrEmpty(p) && Directory.Exists(p)) return p;
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
|
||||
if (Directory.Exists(FallbackRoot)) return FallbackRoot;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
using Rhino;
|
||||
using Rhino.PlugIns;
|
||||
|
||||
namespace DOSSIER;
|
||||
|
||||
/// <summary>
|
||||
/// DOSSIER-Plugin. Drei Aufgaben:
|
||||
/// 1. Bootstrappt beim Plugin-Load die Python-Module: Panels, Aliases,
|
||||
/// View-Modes, BeginCommand-Hook, Welcome-Screen (alles ueber rhino/startup.py).
|
||||
/// 2. Registriert native Rhino-Commands (dWall, dDoor, dStair, ...) die
|
||||
/// jeweils das passende Python-Script in rhino/aliases/cmd/ ausfuehren.
|
||||
/// 3. Loest das Echo-/Autocomplete-Problem der frueheren Keyboard-Macros
|
||||
/// (jetzt zeigt die History "dWall" statt "_-RunPythonScript ...").
|
||||
///
|
||||
/// Installation: Plugin via _PluginManager → Install... registrieren. Beim
|
||||
/// naechsten Rhino-Start laeuft DOSSIER automatisch. Kein zusaetzlicher
|
||||
/// StartupCommands-XML-Eintrag noetig.
|
||||
/// </summary>
|
||||
public class DossierPlugin : PlugIn
|
||||
{
|
||||
public DossierPlugin() { Instance = this; }
|
||||
|
||||
public static DossierPlugin Instance { get; private set; } = null!;
|
||||
|
||||
/// <summary>Plugin bei jedem Rhino-Start automatisch laden — default ist
|
||||
/// "WhenNeeded" (erst beim ersten Command-Aufruf). Wir brauchen aber
|
||||
/// AtStartup, damit OnLoad → startup.py-Bootstrap immer feuert.</summary>
|
||||
public override PlugInLoadTime LoadTime => PlugInLoadTime.AtStartup;
|
||||
|
||||
protected override LoadReturnCode OnLoad(ref string errorMessage)
|
||||
{
|
||||
var root = DossierPaths.Root;
|
||||
if (root is null)
|
||||
{
|
||||
errorMessage =
|
||||
"DOSSIER Root nicht gefunden. Setze Env-Var DOSSIER_HOME " +
|
||||
"auf den DOSSIER-Repo-Ordner (z.B. /Users/karim/STUDIO/DOSSIER) " +
|
||||
"oder leg ein File ~/.dossier_home an.";
|
||||
return LoadReturnCode.ErrorShowDialog;
|
||||
}
|
||||
RhinoApp.WriteLine($"[DOSSIER] Plugin geladen (root={root})");
|
||||
|
||||
// Python-Bootstrap deferred auf Idle — OnLoad feuert vor Eto-UI-Init,
|
||||
// Panels brauchen aber MainWindow + Idle-Event. PythonRunner.RunDeferred
|
||||
// wartet auf naechstes Idle und ruft dann startup.py auf.
|
||||
PythonRunner.RunDeferred(DossierPaths.StartupPy, "startup");
|
||||
|
||||
return LoadReturnCode.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
using System.IO;
|
||||
using Rhino;
|
||||
using Rhino.Commands;
|
||||
|
||||
namespace DOSSIER;
|
||||
|
||||
/// <summary>
|
||||
/// Abstrakte Basis fuer alle DOSSIER-Commands die ein Python-Script
|
||||
/// ausfuehren. Subklasse setzt nur EnglishName + ScriptRelativePath.
|
||||
///
|
||||
/// Mechanik: Rhino erlaubt kein synchrones Command-in-Command-Nesting fuer
|
||||
/// _-RunPythonScript. PythonRunner.RunDeferred wartet auf das naechste Idle-
|
||||
/// Event und versucht dann zuerst die RhinoCode-API (keine Echo) und faellt
|
||||
/// auf _-RunPythonScript zurueck. Der Outer-Command beendet sauber mit Success,
|
||||
/// das Python-Script laeuft direkt danach. Ergebnis fuer den User: sauberer
|
||||
/// Command-Name in der History (z.B. "dWall") statt "_-RunPythonScript ...".
|
||||
/// </summary>
|
||||
public abstract class DossierPythonCommand : Command
|
||||
{
|
||||
/// <summary>z.B. "cmd/wand.py" oder "view/plan.py" — relativ zu rhino/aliases/.</summary>
|
||||
protected abstract string ScriptRelativePath { get; }
|
||||
|
||||
protected override Result RunCommand(RhinoDoc doc, RunMode mode)
|
||||
{
|
||||
var scriptPath = Path.Combine(DossierPaths.AliasDir, ScriptRelativePath);
|
||||
if (!File.Exists(scriptPath))
|
||||
{
|
||||
RhinoApp.WriteLine($"[DOSSIER] FEHLER: Script nicht gefunden: {scriptPath}");
|
||||
return Result.Failure;
|
||||
}
|
||||
PythonRunner.RunDeferred(scriptPath, EnglishName);
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
using System;
|
||||
using System.IO;
|
||||
using Rhino;
|
||||
|
||||
namespace DOSSIER;
|
||||
|
||||
/// <summary>
|
||||
/// Geteilter Python-Script-Runner fuer Plugin-Startup (startup.py) und
|
||||
/// Commands (cmd/*.py). Primaer ueber Rhino.Runtime.Code-API (CPython3),
|
||||
/// Fallback ueber _-RunPythonScript-Command.
|
||||
/// </summary>
|
||||
internal static class PythonRunner
|
||||
{
|
||||
/// <summary>Fuehrt das Script aus. Versucht RhinoCode-API zuerst,
|
||||
/// faellt auf _-RunPythonScript zurueck. Labels nur fuer Logs.</summary>
|
||||
public static bool Run(string scriptPath, string label)
|
||||
{
|
||||
if (!File.Exists(scriptPath))
|
||||
{
|
||||
RhinoApp.WriteLine($"[DOSSIER] {label}: Script nicht gefunden: {scriptPath}");
|
||||
return false;
|
||||
}
|
||||
if (TryRunViaRhinoCode(scriptPath, label)) return true;
|
||||
try
|
||||
{
|
||||
RhinoApp.RunScript($"_-RunPythonScript \"{scriptPath}\"", echo: false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RhinoApp.WriteLine($"[DOSSIER] {label} RunScript: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Wie Run, aber defern auf das naechste Idle-Event.
|
||||
/// Erlaubt safe Invocation aus Plugin-OnLoad oder Command-RunCommand
|
||||
/// (Rhino mag kein direktes _-RunPythonScript aus diesen Kontexten).</summary>
|
||||
public static void RunDeferred(string scriptPath, string label)
|
||||
{
|
||||
EventHandler? handler = null;
|
||||
handler = (sender, e) =>
|
||||
{
|
||||
RhinoApp.Idle -= handler;
|
||||
Run(scriptPath, label);
|
||||
};
|
||||
RhinoApp.Idle += handler;
|
||||
}
|
||||
|
||||
private static bool TryRunViaRhinoCode(string scriptPath, string label)
|
||||
{
|
||||
try
|
||||
{
|
||||
var spec = new Rhino.Runtime.Code.Languages.LanguageSpec("*.*.python", "3.*");
|
||||
var lang = Rhino.Runtime.Code.RhinoCode.Languages.QueryLatest(spec);
|
||||
if (lang == null) return false;
|
||||
|
||||
// RhinoCode.CreateCode(text) setzt __file__/sys.path NICHT automatisch
|
||||
// — die DOSSIER-Scripts erwarten beides. Injizieren vorne rein.
|
||||
var pathLit = scriptPath.Replace("\\", "/");
|
||||
var preamble =
|
||||
"import sys, os\n" +
|
||||
$"__file__ = r'{pathLit}'\n" +
|
||||
"sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n" +
|
||||
"sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n";
|
||||
var code = lang.CreateCode(preamble + File.ReadAllText(scriptPath));
|
||||
var ctx = new Rhino.Runtime.Code.Execution.RunContext();
|
||||
code.Run(ctx);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RhinoApp.WriteLine($"[DOSSIER] {label} RhinoCode: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/bin/bash
|
||||
# DOSSIER — Build-Skript
|
||||
# Baut das C#-Plugin (.rhp) das beim Rhino-Start die Python-Module
|
||||
# bootstrappt (Panels, Aliases, Welcome) und Native-Commands registriert
|
||||
# (dWall, dDoor, dStair, dSlab, ...).
|
||||
#
|
||||
# === Voraussetzungen ===
|
||||
# 1. .NET 7 SDK installiert. Auf Mac:
|
||||
# brew install dotnet@7
|
||||
# Oder direkt von Microsoft:
|
||||
# https://dotnet.microsoft.com/download/dotnet/7.0
|
||||
#
|
||||
# 2. RhinoCommon NuGet-Package wird beim ersten Build automatisch geladen.
|
||||
#
|
||||
# === Build ===
|
||||
# ./build.sh — Release-Build, output in bin/Release/net7.0/
|
||||
# ./build.sh debug — Debug-Build mit Symbols
|
||||
# ./build.sh clean — bin/obj loeschen
|
||||
# ./build.sh install — Build + ins Rhino Plug-In-Verzeichnis kopieren
|
||||
#
|
||||
# === Installation in Rhino (einmalig, auf Mac) ===
|
||||
# WICHTIG: Mac Rhino 8 unterstuetzt KEIN Drag-Drop fuer .rhp-Plugins
|
||||
# (der Drag landet im Datei-Oeffnen-Handler, nicht im Plugin-Loader).
|
||||
#
|
||||
# Richtiger Weg:
|
||||
# 1. Rhino 8 oeffnen
|
||||
# 2. Command-Prompt: PluginManager
|
||||
# (oder Tools-Menue → Options → Plug-Ins)
|
||||
# 3. Button "Install..." → browse zur .rhp
|
||||
# bin/Release/net7.0/DOSSIER.rhp
|
||||
# 4. Open → Rhino registriert das Plugin
|
||||
# 5. Rhino restart — DOSSIER bootstrappt (Panels/Aliases/Welcome) +
|
||||
# Commands dWall/dDoor/... sind verfuegbar
|
||||
#
|
||||
# Pfad bleibt in Rhinos settings-XML registriert. Bei spaeteren Builds
|
||||
# einfach in den gleichen Output-Pfad bauen — Rhino laedt den neuen Stand
|
||||
# automatisch beim naechsten Start.
|
||||
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# --- Param-Parsing ---
|
||||
MODE="${1:-release}"
|
||||
|
||||
case "$MODE" in
|
||||
debug|Debug)
|
||||
CONFIG="Debug"
|
||||
;;
|
||||
release|Release|"")
|
||||
CONFIG="Release"
|
||||
;;
|
||||
clean)
|
||||
echo "==> Loesche bin/ + obj/"
|
||||
rm -rf bin obj
|
||||
exit 0
|
||||
;;
|
||||
install)
|
||||
CONFIG="Release"
|
||||
DO_INSTALL=1
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [release|debug|clean|install]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- dotnet-Check ---
|
||||
if ! command -v dotnet &>/dev/null; then
|
||||
echo "FEHLER: dotnet nicht installiert."
|
||||
echo "Install: brew install dotnet@7"
|
||||
echo " oder https://dotnet.microsoft.com/download/dotnet/7.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Build ---
|
||||
echo "==> Build: $CONFIG"
|
||||
dotnet build -c "$CONFIG"
|
||||
|
||||
OUTPUT="$SCRIPT_DIR/bin/$CONFIG/net7.0/DOSSIER.rhp"
|
||||
if [ ! -f "$OUTPUT" ]; then
|
||||
echo "FEHLER: Build-Output nicht gefunden: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
echo "==> .rhp Output: $OUTPUT"
|
||||
|
||||
# --- Yak-Paket bauen ---
|
||||
# yak (Rhinos Package Manager) ist im Rhino-App-Bundle dabei. Wir packen .rhp +
|
||||
# manifest.yml in ein .yak-Archiv das der Launcher bundlet + via "yak install"
|
||||
# in den User-Plugin-Pfad legt. Dort wird's von Rhino aus dem trusted Yak-
|
||||
# Verzeichnis geladen.
|
||||
YAK="/Applications/Rhino 8.app/Contents/Resources/bin/yak"
|
||||
DIST_DIR="$SCRIPT_DIR/dist"
|
||||
mkdir -p "$DIST_DIR"
|
||||
if [ -x "$YAK" ]; then
|
||||
BUILD_DIR="$SCRIPT_DIR/bin/$CONFIG/net7.0"
|
||||
pushd "$BUILD_DIR" >/dev/null
|
||||
# yak spec failed mit Exit-1 wenn manifest.yml schon existiert — kein Fehler
|
||||
"$YAK" spec --input DOSSIER.rhp >/dev/null 2>&1 || true
|
||||
rm -f dossier-*.yak
|
||||
YAK_OUT=$("$YAK" build 2>&1 | grep -oE '/.*\.yak$' | head -1)
|
||||
popd >/dev/null
|
||||
if [ -n "$YAK_OUT" ] && [ -f "$YAK_OUT" ]; then
|
||||
# Versionierter Filename rein damit Launcher die Version vom Filename ablesen kann
|
||||
YAK_NAME=$(basename "$YAK_OUT")
|
||||
# Alte .yak im dist/ wegraeumen
|
||||
rm -f "$DIST_DIR"/dossier-*.yak
|
||||
cp -v "$YAK_OUT" "$DIST_DIR/$YAK_NAME"
|
||||
# Stabilen Symlink fuer Launcher (immer 'dossier.yak') zusaetzlich
|
||||
ln -sf "$YAK_NAME" "$DIST_DIR/dossier.yak"
|
||||
# Version separat als Textdatei (extrahiert aus manifest.yml im .yak)
|
||||
VERSION=$(grep '^version:' "$BUILD_DIR/manifest.yml" | awk '{print $2}')
|
||||
echo -n "$VERSION" > "$DIST_DIR/dossier-version.txt"
|
||||
echo "==> .yak Output: $DIST_DIR/$YAK_NAME (version=$VERSION)"
|
||||
else
|
||||
echo "WARN: yak build hat keinen Output produziert"
|
||||
fi
|
||||
else
|
||||
echo "WARN: yak CLI nicht gefunden ($YAK) — kein .yak-Paket gebaut"
|
||||
fi
|
||||
|
||||
# --- Install: lokales Test-Install via yak ---
|
||||
# Fuer Dev-Iteration: installiert das frische .yak direkt in den Rhino-User-
|
||||
# Plugin-Pfad (~/Library/Application Support/McNeel/Rhinoceros/packages/8.0/).
|
||||
# In Production macht der Launcher das automatisch beim ersten Rhino-Start.
|
||||
if [ -n "$DO_INSTALL" ]; then
|
||||
# Alte Manuell-Install-Standorte aufraeumen
|
||||
OLD_MANUAL="$HOME/Library/Application Support/Dossier/Plugin"
|
||||
for old in "/Applications/Rhino 8.app/Contents/PlugIns/DOSSIER.rhp" \
|
||||
"/Applications/Rhino 8.app/Contents/PlugIns/DossierCommands.rhp" \
|
||||
"$OLD_MANUAL/DOSSIER.rhp"; do
|
||||
if [ -f "$old" ]; then rm -v "$old"; fi
|
||||
done
|
||||
if [ -x "$YAK" ] && [ -f "$DIST_DIR/dossier.yak" ]; then
|
||||
# yak install nimmt Quelle als Verzeichnis (treats local dir as source server)
|
||||
"$YAK" install dossier --source "$DIST_DIR" 2>&1 | sed 's/^/ /'
|
||||
echo "==> Yak-Install fertig. Rhino restart noetig (Plugin laedt on-demand beim ersten Command)."
|
||||
echo "==> StartupCommands-XML-Eintrag wird vom Launcher gesetzt — fuer Dev manuell pruefen:"
|
||||
echo " Options → General → Run these commands every time a model is opened"
|
||||
echo " soll enthalten: _-RunPythonScript \"$SCRIPT_DIR/../../rhino/startup.py\""
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "OK."
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ Damit die Module bei jedem Rhino-Start automatisch laden:
|
||||
2. `Rhinoceros 8` → `Preferences` → `General` → **Startup commands**
|
||||
3. Folgende Zeile eintragen:
|
||||
```
|
||||
_-RunPythonScript "/Users/karim/STUDIO/rhino-panel/rhino/startup.py"
|
||||
_-RunPythonScript "/Users/karim/STUDIO/DOSSIER/rhino/startup.py"
|
||||
```
|
||||
4. OK → Rhino neu starten
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dossier</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=Playfair+Display:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.6.3",
|
||||
"notes": "Dossier 0.6.3",
|
||||
"pub_date": "2026-05-19T10:11:20Z",
|
||||
"platforms": {
|
||||
"darwin-aarch64": {
|
||||
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVRNFYzbUN3TE44QnR0RlRXY3c2MGloMTY4L0hja3ZaOGVYbHc4VHY5MHU3V0NqQ2o5aTlyWFJwaWc0RHV4OXNYRGsrZWN4eGdnSzhpTENQRy95NzZONGFiV2cwNWVpdnd3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc5MTg1NDgwCWZpbGU6RG9zc2llci5hcHAudGFyLmd6CmRKZlBWWTZMSUI5L1VmUmh4QnZoUkxXTU5zQ0pMWUpyUk4yQUZ2SGtzaUF2WTl2ZU8xM21saElqT1ppeldxTmpGUUhGZkJtN0t3RFIyakwvMFByK0F3PT0K",
|
||||
"url": "https://git.kgva.ch/karim/DOSSIER/releases/download/0.6.3/Dossier.app.tar.gz"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+22
-2
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"name": "dossier-launcher",
|
||||
"version": "0.1.0",
|
||||
"version": "0.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dossier-launcher",
|
||||
"version": "0.1.0",
|
||||
"version": "0.5.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6"
|
||||
},
|
||||
@@ -1058,6 +1060,24 @@
|
||||
"@tauri-apps/api": "^2.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-process": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz",
|
||||
"integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-updater": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz",
|
||||
"integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "dossier-launcher",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.6.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,6 +13,8 @@
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Dossier lädt</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--accent: #5fa896;
|
||||
--accent-soft: #6fb5a3;
|
||||
--accent-deep: #2f5d54;
|
||||
--paper: #ffffff;
|
||||
--paper-mute: rgba(255, 255, 255, 0.72);
|
||||
--paper-faint: rgba(255, 255, 255, 0.45);
|
||||
--font-display: Krungthep, 'Archivo Black', sans-serif;
|
||||
--font-mono: 'DM Mono', 'Menlo', monospace;
|
||||
}
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent !important;
|
||||
color: var(--paper);
|
||||
overflow: hidden;
|
||||
font-family: var(--font-mono);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
.frame {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 24px 28px 22px;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 0;
|
||||
background:
|
||||
radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
|
||||
border-radius: 16px;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.18);
|
||||
}
|
||||
.brand-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.brand {
|
||||
font-family: var(--font-display);
|
||||
font-size: 30px;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1;
|
||||
color: var(--paper);
|
||||
}
|
||||
.brand-dot {
|
||||
color: var(--accent-deep);
|
||||
}
|
||||
.version {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.10em;
|
||||
color: var(--paper-mute);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.status-row {
|
||||
align-self: end;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.10em;
|
||||
color: var(--paper);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.dot-pulse {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--paper);
|
||||
animation: pulse 1.6s ease-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(255,255,255,0.55); transform: scale(1); }
|
||||
70% { box-shadow: 0 0 0 9px rgba(255,255,255,0); transform: scale(1.05); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(255,255,255,0); transform: scale(1); }
|
||||
}
|
||||
.bar {
|
||||
position: relative;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.bar::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -35%;
|
||||
width: 35%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, var(--paper), transparent);
|
||||
animation: slide 1.6s linear infinite;
|
||||
}
|
||||
@keyframes slide {
|
||||
to { left: 100%; }
|
||||
}
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
font-size: 9.5px;
|
||||
letter-spacing: 0.14em;
|
||||
color: var(--paper-faint);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<div class="brand-row">
|
||||
<div class="brand">DOSSIER<span class="brand-dot">.</span></div>
|
||||
<div class="version">v0.6.3</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="status-row">
|
||||
<span class="dot-pulse"></span>
|
||||
<span>Plugin lädt — Panels werden platziert</span>
|
||||
</div>
|
||||
<div class="bar"></div>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span>AGPL-3.0 · Karim Gabriele Varano</span>
|
||||
<span>Rhino 8 · CPy 3.9</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
# clean-rhino.sh — setzt DOSSIER in Rhino zurueck auf "frisch installiert" Zustand.
|
||||
# Damit kann das Setup im Launcher (Settings → Setup tab) jederzeit von Null
|
||||
# durchgespielt werden.
|
||||
#
|
||||
# Aufgaben:
|
||||
# 1. yak uninstall dossier (Plugin raus)
|
||||
# 2. Window-Layout-Datei loeschen (workspaces/<guid>.xml)
|
||||
# 3. StartupCommands-XML-Eintrag entfernen (Python-Bootstrap-Trigger)
|
||||
#
|
||||
# Bleibt unangetastet:
|
||||
# - dossier_settings.json (User-Praeferenzen, Tags, etc.)
|
||||
# - launcher recent.json
|
||||
# - alles ausserhalb DOSSIER
|
||||
|
||||
set -e
|
||||
|
||||
RHINO_APP="/Applications/Rhino 8.app"
|
||||
YAK="$RHINO_APP/Contents/Resources/bin/yak"
|
||||
SETTINGS_XML="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"
|
||||
WORKSPACES_DIR="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces"
|
||||
LAYOUT_GUID="b6b68c03-3031-4899-bca2-fe6e425146fc"
|
||||
|
||||
# --- Safety: Rhino muss zu sein ---
|
||||
if pgrep -f "Rhino 8.app/Contents/MacOS/Rhinoceros$" >/dev/null; then
|
||||
echo "FEHLER: Rhino laeuft. Bitte beenden und nochmal."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 1. Yak uninstall (idempotent — meldet 'package not installed' wenn schon weg) ---
|
||||
echo "==> 1. Yak uninstall dossier"
|
||||
if [ -x "$YAK" ]; then
|
||||
"$YAK" uninstall dossier 2>&1 | sed 's/^/ /' || true
|
||||
else
|
||||
echo " WARN: yak nicht gefunden — skip"
|
||||
fi
|
||||
|
||||
# --- 2. Window-Layout-Datei loeschen ---
|
||||
echo "==> 2. Window-Layout-Datei loeschen"
|
||||
LAYOUT_FILE="$WORKSPACES_DIR/$LAYOUT_GUID.xml"
|
||||
if [ -f "$LAYOUT_FILE" ]; then
|
||||
rm -v "$LAYOUT_FILE" | sed 's/^/ /'
|
||||
else
|
||||
echo " schon weg"
|
||||
fi
|
||||
|
||||
# --- 3. StartupCommands-Eintrag aus XML entfernen ---
|
||||
echo "==> 3. StartupCommands-Eintrag entfernen"
|
||||
if [ -f "$SETTINGS_XML" ]; then
|
||||
# sed: matche genau unsere DOSSIER-Zeile und loesche
|
||||
# (egal welcher Pfad — solange startup.py drin steht)
|
||||
if grep -q 'StartupCommands.*startup.py' "$SETTINGS_XML"; then
|
||||
# macOS sed braucht leeres Backup-Suffix
|
||||
sed -i '' '/<entry key="StartupCommands">.*startup\.py.*<\/entry>/d' "$SETTINGS_XML"
|
||||
echo " entfernt"
|
||||
else
|
||||
echo " schon weg"
|
||||
fi
|
||||
else
|
||||
echo " WARN: Rhino-settings-XML nicht gefunden"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Clean fertig. Naechster Schritt:"
|
||||
echo " → Launcher → Settings → Setup → 'Setup starten'"
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build + sign Dossier-Launcher fuer den Updater, und emit latest.json.
|
||||
#
|
||||
# Voraussetzungen:
|
||||
# - Private Key liegt unter ~/.tauri/dossier_updater.key
|
||||
# (einmalig erzeugen mit:
|
||||
# npx tauri signer generate -w ~/.tauri/dossier_updater.key)
|
||||
# - Version wurde in src-tauri/tauri.conf.json + package.json hochgezaehlt
|
||||
#
|
||||
# Ablauf:
|
||||
# 1) npx tauri build (mit Signing-Env)
|
||||
# 2) liest die erzeugte .sig-Datei
|
||||
# 3) schreibt latest.json im launcher-Root mit URLs auf Gitea-Release-Assets
|
||||
#
|
||||
# Danach manuell:
|
||||
# - auf Gitea einen Release mit Tag <VERSION> erstellen
|
||||
# - die .app.tar.gz und (optional) die .dmg als Assets hochladen
|
||||
# - latest.json committen + auf main pushen
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
KEY_PATH="${TAURI_SIGNING_PRIVATE_KEY_PATH:-$HOME/.tauri/dossier_updater.key}"
|
||||
GITEA_REPO="https://git.kgva.ch/karim/DOSSIER"
|
||||
|
||||
# WICHTIG: `npx tauri ...` ohne Namespace zieht ausserhalb dieses Verzeichnisses
|
||||
# das uralte tauri@0.15 von npm (das kein `signer` kennt). Wir nutzen den
|
||||
# expliziten Paketnamen @tauri-apps/cli — der funktioniert von ueberall und
|
||||
# auch innerhalb dieses Verzeichnisses, wo die devDependency ohnehin vorhanden ist.
|
||||
TAURI_CLI="npx --yes @tauri-apps/cli"
|
||||
|
||||
if [ ! -f "$KEY_PATH" ]; then
|
||||
echo "Private Key fehlt: $KEY_PATH" >&2
|
||||
echo "Einmalig erzeugen mit:" >&2
|
||||
echo " $TAURI_CLI signer generate -w $KEY_PATH" >&2
|
||||
echo "und den public key (.pub) in src-tauri/tauri.conf.json -> plugins.updater.pubkey eintragen." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
if [ "$VERSION" != "$PKG_VERSION" ]; then
|
||||
echo "Version mismatch: tauri.conf.json=$VERSION package.json=$PKG_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
arm64|aarch64) PLATFORM_KEY="darwin-aarch64" ;;
|
||||
x86_64) PLATFORM_KEY="darwin-x86_64" ;;
|
||||
*) echo "Unsupported arch: $ARCH" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "→ Build Dossier $VERSION ($PLATFORM_KEY)"
|
||||
TAURI_SIGNING_PRIVATE_KEY="$(cat "$KEY_PATH")" \
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD="" \
|
||||
$TAURI_CLI build
|
||||
|
||||
BUNDLE_DIR="src-tauri/target/release/bundle/macos"
|
||||
TAR_GZ=$(ls "$BUNDLE_DIR"/*.app.tar.gz 2>/dev/null | head -n1 || true)
|
||||
SIG_FILE="${TAR_GZ}.sig"
|
||||
|
||||
if [ -z "$TAR_GZ" ] || [ ! -f "$SIG_FILE" ]; then
|
||||
echo "Bundle oder Signatur fehlt in $BUNDLE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ASSET_NAME=$(basename "$TAR_GZ")
|
||||
ASSET_URL_NAME=$(printf '%s' "$ASSET_NAME" | sed 's/ /%20/g')
|
||||
SIGNATURE=$(cat "$SIG_FILE")
|
||||
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
DOWNLOAD_URL="$GITEA_REPO/releases/download/$VERSION/$ASSET_URL_NAME"
|
||||
|
||||
NOTES=${RELEASE_NOTES:-"Dossier $VERSION"}
|
||||
|
||||
cat > latest.json <<EOF
|
||||
{
|
||||
"version": "$VERSION",
|
||||
"notes": $(node -e "process.stdout.write(JSON.stringify(process.argv[1]))" "$NOTES"),
|
||||
"pub_date": "$PUB_DATE",
|
||||
"platforms": {
|
||||
"$PLATFORM_KEY": {
|
||||
"signature": $(node -e "process.stdout.write(JSON.stringify(process.argv[1]))" "$SIGNATURE"),
|
||||
"url": "$DOWNLOAD_URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo
|
||||
echo "✓ Build fertig"
|
||||
echo " Bundle: $TAR_GZ"
|
||||
echo " Signatur: $SIG_FILE"
|
||||
echo " DMG: $(ls src-tauri/target/release/bundle/dmg/*.dmg 2>/dev/null | head -n1 || echo '(keine DMG gefunden)')"
|
||||
echo " Platform: $PLATFORM_KEY"
|
||||
echo " latest.json wurde im launcher-Root geschrieben."
|
||||
echo
|
||||
echo "Nächste Schritte:"
|
||||
echo " 1) Auf Gitea Release mit Tag $VERSION erstellen und folgende Assets hochladen:"
|
||||
echo " - $ASSET_NAME"
|
||||
echo " - (optional) DMG für Erstinstallation"
|
||||
echo " 2) latest.json committen + auf main pushen:"
|
||||
echo " git add launcher/latest.json && git commit -m 'Release $VERSION' && git push origin main"
|
||||
Generated
+494
-7
@@ -47,6 +47,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.18.2"
|
||||
@@ -124,6 +133,12 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -323,6 +338,35 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"core-foundation",
|
||||
"core-graphics 0.24.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -359,6 +403,19 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.25.0"
|
||||
@@ -520,6 +577,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -656,15 +724,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dossier-launcher"
|
||||
version = "0.1.0"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cocoa",
|
||||
"directories",
|
||||
"objc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-updater",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -755,6 +827,16 @@ dependencies = [
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
@@ -780,6 +862,16 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -1322,6 +1414,21 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
@@ -1577,6 +1684,36 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys 0.4.1",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.18",
|
||||
"walkdir",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
@@ -1714,6 +1851,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
@@ -1735,6 +1878,15 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.38.0"
|
||||
@@ -1767,6 +1919,12 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -1876,6 +2034,15 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||
dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
@@ -2015,6 +2182,18 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
@@ -2078,12 +2257,32 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -2465,15 +2664,20 @@ dependencies = [
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -2509,6 +2713,20 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
@@ -2524,6 +2742,92 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||
dependencies = [
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"jni 0.22.4",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
@@ -2539,6 +2843,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
@@ -2596,6 +2909,29 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.36.1"
|
||||
@@ -2806,6 +3142,22 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
@@ -2918,6 +3270,12 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -2992,7 +3350,7 @@ dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"core-graphics 0.25.0",
|
||||
"crossbeam-channel",
|
||||
"dbus",
|
||||
"dispatch2",
|
||||
@@ -3001,7 +3359,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
@@ -3034,6 +3392,17 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -3057,7 +3426,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -3211,6 +3580,49 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-process"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
|
||||
dependencies = [
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.11.1"
|
||||
@@ -3221,7 +3633,7 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
@@ -3244,7 +3656,7 @@ checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
@@ -3311,6 +3723,19 @@ dependencies = [
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tendril"
|
||||
version = "0.5.0"
|
||||
@@ -3431,6 +3856,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
@@ -3727,6 +4162,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -4019,6 +4460,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
@@ -4258,6 +4708,15 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
@@ -4698,7 +5157,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"ndk",
|
||||
"objc2",
|
||||
@@ -4745,6 +5204,16 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
@@ -4789,6 +5258,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.4"
|
||||
@@ -4822,6 +5297,18 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.14.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[package]
|
||||
name = "dossier-launcher"
|
||||
version = "0.1.0"
|
||||
version = "0.6.3"
|
||||
description = "Dossier — Projekt-Launcher fuer Rhino"
|
||||
authors = ["Karim Gabriele Varano"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -13,13 +14,22 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-process = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
directories = "5"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# macOS-only: native NSWindow-Calls fuer abgerundete Splash-Ecken.
|
||||
# Cocoa + objc sind die etablierten low-level Bindings; Tauris transparent:true
|
||||
# Window haette sonst weisse Ecken weil WkWebView per default opaque ist.
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.26"
|
||||
objc = "0.2"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability fuer das Hauptfenster",
|
||||
"windows": ["main"],
|
||||
"description": "Capability fuer Haupt- und Splash-Fenster",
|
||||
"windows": ["main", "splash"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"dialog:default"
|
||||
"core:webview:allow-print",
|
||||
"dialog:default",
|
||||
"updater:default",
|
||||
"process:allow-restart"
|
||||
]
|
||||
}
|
||||
|
||||
+1146
-14
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
// 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() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Dossier",
|
||||
"version": "0.1.0",
|
||||
"version": "0.6.3",
|
||||
"identifier": "ch.gabrielevarano.dossier",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
@@ -14,12 +14,28 @@
|
||||
{
|
||||
"label": "main",
|
||||
"title": "Dossier",
|
||||
"width": 920,
|
||||
"height": 640,
|
||||
"minWidth": 720,
|
||||
"minHeight": 480,
|
||||
"width": 1080,
|
||||
"height": 720,
|
||||
"minWidth": 880,
|
||||
"minHeight": 520,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
},
|
||||
{
|
||||
"label": "splash",
|
||||
"url": "splash.html",
|
||||
"title": "Dossier lädt",
|
||||
"width": 440,
|
||||
"height": 190,
|
||||
"center": true,
|
||||
"alwaysOnTop": true,
|
||||
"decorations": false,
|
||||
"resizable": false,
|
||||
"skipTaskbar": true,
|
||||
"visible": false,
|
||||
"transparent": true,
|
||||
"shadow": true,
|
||||
"focus": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
@@ -29,14 +45,28 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["app", "dmg"],
|
||||
"createUpdaterArtifacts": true,
|
||||
"icon": ["icons/icon.png"],
|
||||
"copyright": "© 2026 Karim Gabriele Varano",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "Dossier Launcher",
|
||||
"longDescription": "Projekt-Launcher fuer das Dossier-Plugin in Rhino 8.",
|
||||
"macOS": {
|
||||
"signingIdentity": "-"
|
||||
},
|
||||
"resources": {
|
||||
"../../dist": "dist",
|
||||
"../../rhino": "rhino"
|
||||
"../../rhino": "rhino",
|
||||
"../../csharp/DOSSIER/dist/dossier.yak": "plugin/dossier.yak",
|
||||
"../../csharp/DOSSIER/dist/dossier-version.txt": "plugin/dossier-version.txt"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"endpoints": [
|
||||
"https://git.kgva.ch/karim/DOSSIER/raw/branch/main/latest.json"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDY3Q0IzQzA4Mjc5NTczOApSV1E0VjNtQ3dMTjhCamZqbElWdDBlQnNNU3ZEZDg0bEp0aGtyRnN1M2ZKZTdJYzV0TUJEUnhxRQo="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1972
-85
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
// Material-Symbols-Outlined-style Icons als Inline-SVG. Keine Font-Loads,
|
||||
// kein Codepoint-Mapping — sauber zu themen via currentColor + stroke-width.
|
||||
|
||||
const PATHS = {
|
||||
folder: (
|
||||
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" />
|
||||
),
|
||||
edit: (
|
||||
<>
|
||||
<path d="M4 20h4l10.5-10.5a2.121 2.121 0 0 0-3-3L5 17v3Z" />
|
||||
<path d="m13.5 6.5 3 3" />
|
||||
</>
|
||||
),
|
||||
plus: (
|
||||
<>
|
||||
<path d="M12 5v14" />
|
||||
<path d="M5 12h14" />
|
||||
</>
|
||||
),
|
||||
search: (
|
||||
<>
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<path d="m20 20-3.5-3.5" />
|
||||
</>
|
||||
),
|
||||
close: (
|
||||
<>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="M6 6l12 12" />
|
||||
</>
|
||||
),
|
||||
settings: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c.36.13.73.31 1.05.55" />
|
||||
</>
|
||||
),
|
||||
layers: (
|
||||
<>
|
||||
<path d="m12 2 9 5-9 5-9-5 9-5Z" />
|
||||
<path d="m3 12 9 5 9-5" />
|
||||
<path d="m3 17 9 5 9-5" />
|
||||
</>
|
||||
),
|
||||
trash: (
|
||||
<>
|
||||
<path d="M3 6h18" />
|
||||
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
<path d="m19 6-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
</>
|
||||
),
|
||||
refresh: (
|
||||
<>
|
||||
<path d="M21 12a9 9 0 1 1-3.5-7.1" />
|
||||
<path d="M21 4v5h-5" />
|
||||
</>
|
||||
),
|
||||
pin: (
|
||||
<>
|
||||
<path d="M9 4h6l-1 5 3 3v2H7v-2l3-3-1-5Z" />
|
||||
<path d="M12 14v6" />
|
||||
</>
|
||||
),
|
||||
pin_filled: (
|
||||
<path d="M9 4h6l-1 5 3 3v2h-4v6h-2v-6H7v-2l3-3-1-5Z" fill="currentColor" stroke="none" />
|
||||
),
|
||||
snapshot: (
|
||||
<>
|
||||
<path d="M3 9a2 2 0 0 1 2-2h2.5l1.7-2h5.6l1.7 2H19a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9Z" />
|
||||
<circle cx="12" cy="13" r="3.5" />
|
||||
</>
|
||||
),
|
||||
tag: (
|
||||
<>
|
||||
<path d="M3 12V4h8l10 10-8 8L3 12Z" />
|
||||
<circle cx="8" cy="8" r="1.4" fill="currentColor" stroke="none" />
|
||||
</>
|
||||
),
|
||||
chevron_down: <path d="m6 9 6 6 6-6" />,
|
||||
chevron_up: <path d="m6 15 6-6 6 6" />,
|
||||
};
|
||||
|
||||
export default function Icon({ name, size = 16, strokeWidth = 1.6, style }) {
|
||||
const path = PATHS[name];
|
||||
if (!path) return null;
|
||||
return (
|
||||
<svg
|
||||
width={size} height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{ flexShrink: 0, ...style }}
|
||||
>
|
||||
{path}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { checkForAppUpdate, installAppUpdate, skipUpdateVersion, isTauri } from "../utils/updater.js";
|
||||
|
||||
export default function UpdateNotifier() {
|
||||
const [update, setUpdate] = useState(null);
|
||||
const [state, setState] = useState("idle");
|
||||
const [downloaded, setDownloaded] = useState(0);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const runCheck = useCallback(async ({ silent }) => {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
setState("checking");
|
||||
setError(null);
|
||||
const res = await checkForAppUpdate({ respectSkip: silent });
|
||||
if (!res.available) {
|
||||
setUpdate(null);
|
||||
setState("idle");
|
||||
return;
|
||||
}
|
||||
setUpdate(res.update);
|
||||
setState("available");
|
||||
} catch (e) {
|
||||
console.error("Update-Check fehlgeschlagen:", e);
|
||||
setError(String(e?.message || e));
|
||||
setState("idle");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => runCheck({ silent: true }), 1500);
|
||||
return () => clearTimeout(t);
|
||||
}, [runCheck]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => runCheck({ silent: false });
|
||||
window.addEventListener("dossier:check-update", handler);
|
||||
return () => window.removeEventListener("dossier:check-update", handler);
|
||||
}, [runCheck]);
|
||||
|
||||
const install = async () => {
|
||||
if (!update) return;
|
||||
try {
|
||||
setState("downloading");
|
||||
setDownloaded(0);
|
||||
setTotal(0);
|
||||
await installAppUpdate(update, (event) => {
|
||||
if (event.event === "Started") {
|
||||
setTotal(event.data.contentLength || 0);
|
||||
} else if (event.event === "Progress") {
|
||||
setDownloaded((d) => d + (event.data.chunkLength || 0));
|
||||
} else if (event.event === "Finished") {
|
||||
setState("installing");
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Update-Installation fehlgeschlagen:", e);
|
||||
setError(String(e?.message || e));
|
||||
setState("available");
|
||||
}
|
||||
};
|
||||
|
||||
const skipVersion = () => {
|
||||
skipUpdateVersion(update?.version);
|
||||
setUpdate(null);
|
||||
setState("idle");
|
||||
};
|
||||
|
||||
const later = () => {
|
||||
setUpdate(null);
|
||||
setState("idle");
|
||||
};
|
||||
|
||||
if (!isTauri() || !update || state === "idle" || state === "checking") return null;
|
||||
|
||||
const isBusy = state === "downloading" || state === "installing";
|
||||
const pct = total > 0 ? Math.min(100, Math.round((downloaded / total) * 100)) : null;
|
||||
|
||||
return (
|
||||
<div className="dialog-bg" style={{ zIndex: 300 }}>
|
||||
<div className="dialog" style={{ width: 460, maxWidth: "92vw" }}>
|
||||
<header style={{ background: "var(--bg-panel)" }}>
|
||||
<div style={{ fontSize: 10, letterSpacing: "0.18em", color: "var(--accent)", marginBottom: 4, fontWeight: 600 }}>
|
||||
UPDATE VERFÜGBAR
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
|
||||
<div style={{ fontSize: 18, color: "var(--text)", fontWeight: 500 }}>
|
||||
Dossier {update.version}
|
||||
</div>
|
||||
{update.currentVersion && (
|
||||
<div style={{ fontSize: 11, color: "var(--text-muted)" }}>von {update.currentVersion}</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="body">
|
||||
{update.body && (
|
||||
<div style={{ display: "flex", gap: 12 }}>
|
||||
<div style={{ width: 3, flexShrink: 0, background: "var(--accent)", borderRadius: 2 }} />
|
||||
<div style={{ fontSize: 12, color: "var(--text-muted)", lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{update.body}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div style={{ padding: "10px 12px", background: "rgba(200, 112, 80, 0.12)", border: "1px solid var(--danger)", borderRadius: 6, fontSize: 12, color: "var(--danger)" }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isBusy && (
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-muted)", marginBottom: 6, letterSpacing: "0.04em" }}>
|
||||
{state === "downloading"
|
||||
? (pct !== null ? `Wird heruntergeladen … ${pct}%` : "Wird heruntergeladen …")
|
||||
: "Wird installiert …"}
|
||||
</div>
|
||||
<div style={{ height: 4, background: "var(--bg-elev)", borderRadius: 2, overflow: "hidden" }}>
|
||||
<div style={{
|
||||
height: "100%",
|
||||
width: pct !== null ? `${pct}%` : "100%",
|
||||
background: "var(--accent)",
|
||||
transition: "width 0.2s",
|
||||
animation: pct === null ? "dossier-update-pulse 1.2s ease-in-out infinite" : undefined,
|
||||
}} />
|
||||
</div>
|
||||
<style>{`@keyframes dossier-update-pulse { 0%,100% { opacity: 0.5; } 50% { opacity: 1; } }`}</style>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer style={{ flexDirection: "column", gap: 8, alignItems: "stretch" }}>
|
||||
<button className="primary pill" style={{ width: "100%" }} onClick={install} disabled={isBusy}>
|
||||
{isBusy ? "Bitte warten …" : "Jetzt installieren"}
|
||||
</button>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<button style={{ flex: 1 }} onClick={later} disabled={isBusy}>Später</button>
|
||||
<button style={{ flex: 1 }} onClick={skipVersion} disabled={isBusy}>Diese Version überspringen</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
|
||||
+911
-97
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
// Shared helpers fuer den Tauri-Updater. Verwendet vom Auto-Check Modal
|
||||
// (UpdateNotifier) und dem manuellen Check in den Einstellungen.
|
||||
|
||||
export const SKIP_KEY = "dossier_update_skipped_version";
|
||||
export const LAST_CHECK_KEY = "dossier_update_last_check";
|
||||
|
||||
export function isTauri() {
|
||||
return typeof window !== "undefined" && !!window.__TAURI_INTERNALS__;
|
||||
}
|
||||
|
||||
export async function checkForAppUpdate({ respectSkip = true } = {}) {
|
||||
if (!isTauri()) return { available: false, isTauri: false };
|
||||
const { check } = await import("@tauri-apps/plugin-updater");
|
||||
const result = await check();
|
||||
localStorage.setItem(LAST_CHECK_KEY, new Date().toISOString());
|
||||
if (!result?.available) return { available: false, update: null, isTauri: true };
|
||||
if (respectSkip && localStorage.getItem(SKIP_KEY) === result.version) {
|
||||
return { available: false, update: result, isTauri: true, skipped: true };
|
||||
}
|
||||
return { available: true, update: result, isTauri: true };
|
||||
}
|
||||
|
||||
export async function installAppUpdate(update, onProgress) {
|
||||
await update.downloadAndInstall(onProgress);
|
||||
const { relaunch } = await import("@tauri-apps/plugin-process");
|
||||
await relaunch();
|
||||
}
|
||||
|
||||
export function skipUpdateVersion(version) {
|
||||
if (version) localStorage.setItem(SKIP_KEY, version);
|
||||
}
|
||||
|
||||
export function getLastUpdateCheck() {
|
||||
return localStorage.getItem(LAST_CHECK_KEY);
|
||||
}
|
||||
|
||||
export function formatLastCheck(iso) {
|
||||
if (!iso) return "noch nie";
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString("de-CH", {
|
||||
day: "2-digit", month: "long", year: "numeric",
|
||||
hour: "2-digit", minute: "2-digit",
|
||||
});
|
||||
} catch {
|
||||
return "—";
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "rhino-panel",
|
||||
"version": "0.0.0",
|
||||
"name": "dossier",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rhino-panel",
|
||||
"version": "0.0.0",
|
||||
"name": "dossier",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "dossier",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#! python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""Loescht ALLE Custom-Display-Modes (User-erstellte) — laesst die Rhino-
|
||||
Built-ins (Wireframe, Shaded, Rendered, Ghosted, XRay, Technical, Artistic,
|
||||
Pen, Monochrome, Arctic, Raytraced) in Ruhe.
|
||||
|
||||
Loescht auch Orphan-Modes ohne Namen (die manchmal bei abgebrochenen
|
||||
Imports hierbleiben und Rhino zum Crash bringen wenn man sie anklickt).
|
||||
|
||||
Vorgehen:
|
||||
_RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_clean_display_modes.py
|
||||
"""
|
||||
from Rhino.Display import DisplayModeDescription
|
||||
|
||||
BUILTIN_NAMES = {
|
||||
"Wireframe", "Shaded", "Rendered", "Ghosted",
|
||||
"X-Ray", "XRay", "X Ray",
|
||||
"Technical", "Artistic", "Pen", "Monochrome",
|
||||
"Arctic", "Raytraced",
|
||||
}
|
||||
|
||||
deleted = []
|
||||
kept = []
|
||||
errors = []
|
||||
|
||||
for dm in list(DisplayModeDescription.GetDisplayModes()):
|
||||
name_en = name_local = None
|
||||
try: name_en = dm.EnglishName
|
||||
except Exception: pass
|
||||
try: name_local = dm.LocalName
|
||||
except Exception: pass
|
||||
name_display = name_en or name_local or "(Orphan, kein Name)"
|
||||
is_builtin = (name_en in BUILTIN_NAMES) or (name_local in BUILTIN_NAMES)
|
||||
|
||||
if is_builtin:
|
||||
kept.append(name_display)
|
||||
continue
|
||||
|
||||
# Custom oder Orphan → loeschen
|
||||
try:
|
||||
dm_id = dm.Id
|
||||
ok = DisplayModeDescription.DeleteDisplayMode(dm_id)
|
||||
if ok:
|
||||
deleted.append("{} ({})".format(name_display, dm_id))
|
||||
else:
|
||||
errors.append("{} → DeleteDisplayMode returned False".format(name_display))
|
||||
except Exception as ex:
|
||||
errors.append("{} → {}".format(name_display, ex))
|
||||
|
||||
print("[CLEAN] Display-Modes gesaeubert.")
|
||||
print("[CLEAN] Built-ins behalten ({}):".format(len(kept)))
|
||||
for n in kept:
|
||||
print(" ✓ {}".format(n))
|
||||
print("")
|
||||
print("[CLEAN] Geloescht ({}):".format(len(deleted)))
|
||||
for n in deleted:
|
||||
print(" × {}".format(n))
|
||||
if errors:
|
||||
print("")
|
||||
print("[CLEAN] Fehler ({}):".format(len(errors)))
|
||||
for e in errors:
|
||||
print(" ! {}".format(e))
|
||||
print("")
|
||||
print("[CLEAN] Fertig. Jetzt _reset_panels.py laufen lassen damit der")
|
||||
print("[CLEAN] Plugin den 'Dossier Plan' aus dem Template neu importiert.")
|
||||
@@ -0,0 +1,67 @@
|
||||
#! python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""Boundary/Hatch-Inspector — zeigt was Rhino setzt wenn du via
|
||||
Properties-Panel die Section-Boundary aenderst.
|
||||
|
||||
Vorgehen:
|
||||
1. Objekt selektieren
|
||||
2. In Rhinos Properties → Section Style → Custom → Boundary verstellen
|
||||
(Farbe ändern, Visible toggeln, Width setzen)
|
||||
3. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_inspect_obj_boundary.py
|
||||
4. Output schicken — speziell die Boundary-Properties
|
||||
"""
|
||||
import Rhino
|
||||
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||
if not objs:
|
||||
print("[INSPECT] Bitte Objekt selektieren")
|
||||
else:
|
||||
obj = objs[0]
|
||||
a = obj.Attributes
|
||||
print("[INSPECT] Object {}".format(str(obj.Id)[:8]))
|
||||
print("")
|
||||
print("=== Attributes.SectionAttributesSource ===")
|
||||
try: print(" =", a.SectionAttributesSource)
|
||||
except Exception as ex: print(" err:", ex)
|
||||
print("")
|
||||
print("=== Attributes.GetCustomSectionStyle() — alle Props ===")
|
||||
try:
|
||||
css = a.GetCustomSectionStyle()
|
||||
if css is None:
|
||||
print(" None (kein Custom-SectionStyle)")
|
||||
else:
|
||||
for n in sorted(dir(css)):
|
||||
if n.startswith("_"): continue
|
||||
try:
|
||||
v = getattr(css, n)
|
||||
if callable(v): continue
|
||||
sv = str(v)
|
||||
if len(sv) > 80: sv = sv[:77] + "..."
|
||||
print(" {} = {}".format(n, sv))
|
||||
except Exception as ex:
|
||||
print(" {} <unreadable: {}>".format(n, ex))
|
||||
except Exception as ex:
|
||||
print(" err:", ex)
|
||||
print("")
|
||||
print("=== Layer.GetCustomSectionStyle (Layer-Default) ===")
|
||||
try:
|
||||
lyr = doc.Layers[a.LayerIndex]
|
||||
print(" Layer:", lyr.FullPath)
|
||||
if hasattr(lyr, "GetCustomSectionStyle"):
|
||||
css = lyr.GetCustomSectionStyle()
|
||||
if css is None:
|
||||
print(" Layer hat KEIN Custom-SectionStyle")
|
||||
else:
|
||||
for n in sorted(dir(css)):
|
||||
if n.startswith("_"): continue
|
||||
try:
|
||||
v = getattr(css, n)
|
||||
if callable(v): continue
|
||||
sv = str(v)
|
||||
if len(sv) > 80: sv = sv[:77] + "..."
|
||||
print(" {} = {}".format(n, sv))
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print(" err:", ex)
|
||||
@@ -0,0 +1,112 @@
|
||||
#! python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""Dumpt ALLE Section-/Hatch-relevanten Properties des selektierten Objekts.
|
||||
So sehen wir was Rhino's eigene Section-Style-UI tatsaechlich setzt vs.
|
||||
was unser Plugin-Code setzt.
|
||||
|
||||
Vorgehen:
|
||||
1. Ein 3D-Objekt selektieren (Wand, Box, ...)
|
||||
2. In Rhinos Properties-Panel manuell SectionStyle → Custom mit spezifischen
|
||||
Werten setzen (z.B. Pattern Color=Gruen, Pattern Rotation=20, Pattern
|
||||
Scale=2.4, Boundary Color=Rot, Boundary Width Scale=6) → Apply
|
||||
3. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_inspect_obj_section.py
|
||||
4. Output an Claude
|
||||
"""
|
||||
import Rhino
|
||||
|
||||
|
||||
def _fmt(v):
|
||||
if v is None: return "None"
|
||||
s = str(v)
|
||||
if len(s) > 80: s = s[:77] + "..."
|
||||
return s
|
||||
|
||||
|
||||
def _dump_group(css, prefix, title):
|
||||
"""Dumpt Properties auf css deren Name mit `prefix` (case-insens) anfaengt."""
|
||||
print("--- {} ---".format(title))
|
||||
p_lower = prefix.lower()
|
||||
found = False
|
||||
for n in sorted(dir(css)):
|
||||
if n.startswith("_"): continue
|
||||
if p_lower not in n.lower(): continue
|
||||
try:
|
||||
v = getattr(css, n)
|
||||
if callable(v): continue
|
||||
found = True
|
||||
print(" {:32s} = {}".format(n, _fmt(v)))
|
||||
except Exception as ex:
|
||||
print(" {:32s} = <unreadable: {}>".format(n, ex))
|
||||
if not found:
|
||||
print(" (nichts)")
|
||||
|
||||
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||
if not objs:
|
||||
print("[INSPECT] Bitte ein Objekt selektieren")
|
||||
else:
|
||||
obj = objs[0]
|
||||
a = obj.Attributes
|
||||
print("[INSPECT] Object: {} (Id={})".format(type(obj).__name__, obj.Id))
|
||||
|
||||
# SectionAttributesSource (FromLayer / FromObject)
|
||||
print("")
|
||||
print("=== Attributes ===")
|
||||
try:
|
||||
print(" SectionAttributesSource =", a.SectionAttributesSource)
|
||||
except Exception as ex:
|
||||
print(" SectionAttributesSource err:", ex)
|
||||
try:
|
||||
print(" HatchBackgroundFillColor =", a.HatchBackgroundFillColor)
|
||||
except Exception: pass
|
||||
try:
|
||||
print(" HatchBoundaryVisible =", a.HatchBoundaryVisible)
|
||||
except Exception: pass
|
||||
|
||||
# Custom SectionStyle aus Object
|
||||
print("")
|
||||
print("=== Object.GetCustomSectionStyle() ===")
|
||||
css = None
|
||||
if hasattr(a, "GetCustomSectionStyle"):
|
||||
try:
|
||||
css = a.GetCustomSectionStyle()
|
||||
except Exception as ex:
|
||||
print(" err:", ex)
|
||||
|
||||
if css is None:
|
||||
print(" None (kein Custom-SectionStyle set)")
|
||||
else:
|
||||
print(" Type:", type(css).__name__)
|
||||
print("")
|
||||
# Gruppierte Property-Dumps damit Mapping zu Rhino-UI klar wird
|
||||
_dump_group(css, "Hatch", "Hatch (Pattern, Color, Scale, Rotation)")
|
||||
print("")
|
||||
_dump_group(css, "Boundary", "Boundary (Visible, Color, Width)")
|
||||
print("")
|
||||
_dump_group(css, "Background", "Background (FillColor, FillMode)")
|
||||
print("")
|
||||
# Section-spezifisch (SectionFillRule etc.)
|
||||
print("--- Misc Section ---")
|
||||
for n in ("SectionFillRule", "Name", "Id", "HasUserData", "Index"):
|
||||
if hasattr(css, n):
|
||||
try: print(" {:32s} = {}".format(n, _fmt(getattr(css, n))))
|
||||
except Exception: pass
|
||||
|
||||
# Layer-Default SectionStyle als Vergleich
|
||||
print("")
|
||||
print("=== Layer.GetCustomSectionStyle (Layer-Default) ===")
|
||||
try:
|
||||
lyr = doc.Layers[a.LayerIndex]
|
||||
print(" Layer:", lyr.FullPath, "Color:", lyr.Color)
|
||||
if hasattr(lyr, "GetCustomSectionStyle"):
|
||||
l_css = lyr.GetCustomSectionStyle()
|
||||
if l_css is None:
|
||||
print(" Layer hat KEIN Custom-SectionStyle")
|
||||
else:
|
||||
_dump_group(l_css, "Hatch", "Layer.Hatch")
|
||||
_dump_group(l_css, "Boundary", "Layer.Boundary")
|
||||
_dump_group(l_css, "Background", "Layer.Background")
|
||||
except Exception as ex:
|
||||
print(" err:", ex)
|
||||
@@ -0,0 +1,96 @@
|
||||
#! python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""One-Shot-Diagnose: dumpt alle Properties + Werte des 'Dossier Plan'
|
||||
Display-Modes und exportiert ihn als ini neben dem Skript.
|
||||
|
||||
Vorgehen:
|
||||
1. In Rhinos Display-Mode-Editor: 'Show HiddenLines' AUS schalten +
|
||||
Apply
|
||||
2. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_inspect_plan_mode.py
|
||||
3. Resultat: zeigt alle Hidden/Tangent/Silhouette-Properties +
|
||||
/tmp/dossier_plan_inspect.ini
|
||||
|
||||
So koennen wir sehen welche Property-Namen Mac Rhino tatsaechlich hat.
|
||||
"""
|
||||
import os
|
||||
from Rhino.Display import DisplayModeDescription
|
||||
|
||||
target_name = "Dossier Plan"
|
||||
dmd = None
|
||||
for dm in DisplayModeDescription.GetDisplayModes():
|
||||
if dm.EnglishName == target_name or dm.LocalName == target_name:
|
||||
dmd = dm; break
|
||||
|
||||
if dmd is None:
|
||||
print("[INSPECT] 'Dossier Plan' not found")
|
||||
else:
|
||||
attrs = dmd.DisplayAttributes
|
||||
print("[INSPECT] Mode gefunden: {} (Id={})".format(dmd.EnglishName, dmd.Id))
|
||||
print("")
|
||||
print("=== ALLE DisplayAttributes Properties mit Werten ===")
|
||||
for n in sorted(dir(attrs)):
|
||||
if n.startswith("_"): continue
|
||||
try:
|
||||
v = getattr(attrs, n)
|
||||
if callable(v): continue
|
||||
sv = str(v)
|
||||
if len(sv) > 80: sv = sv[:77] + "..."
|
||||
print(" {} = {}".format(n, sv))
|
||||
except Exception as ex:
|
||||
print(" {} = <unreadable: {}>".format(n, ex))
|
||||
|
||||
print("")
|
||||
print("=== Sub-Objekt Properties (ALLE) ===")
|
||||
# Erst alle Sub-Objekt-Properties autodetect (anything mit "+" im String)
|
||||
sub_names = set()
|
||||
for n in dir(attrs):
|
||||
if n.startswith("_"): continue
|
||||
try:
|
||||
v = getattr(attrs, n)
|
||||
if callable(v): continue
|
||||
if "DisplayPipelineAttributes+" in str(v):
|
||||
sub_names.add(n)
|
||||
except Exception: pass
|
||||
# Plus die expliziten Kandidaten
|
||||
for hard in ("CurveSettings", "ObjectSettings", "ShadingSettings",
|
||||
"MeshSpecificAttributes", "SubObjectDisplayMode",
|
||||
"ViewSpecificAttributes"):
|
||||
if hasattr(attrs, hard): sub_names.add(hard)
|
||||
|
||||
for sub_name in sorted(sub_names):
|
||||
try:
|
||||
sub = getattr(attrs, sub_name)
|
||||
print(" --- {} ---".format(sub_name))
|
||||
for n in sorted(dir(sub)):
|
||||
if n.startswith("_"): continue
|
||||
try:
|
||||
v = getattr(sub, n)
|
||||
if callable(v): continue
|
||||
sv = str(v)
|
||||
if len(sv) > 80: sv = sv[:77] + "..."
|
||||
print(" {} = {}".format(n, sv))
|
||||
except Exception as ex:
|
||||
print(" {} = <unreadable: {}>".format(n, ex))
|
||||
except Exception as ex:
|
||||
print(" {} couldn't be inspected: {}".format(sub_name, ex))
|
||||
|
||||
print("")
|
||||
print("=== ini-Export ===")
|
||||
# In den Desktop schreiben damit der User die Datei einfach manuell
|
||||
# oeffnen + mir den Inhalt schicken kann (in /tmp gehts manchmal verloren).
|
||||
ini_path = os.path.expanduser("~/Desktop/dossier_plan_inspect.ini")
|
||||
try:
|
||||
ok = DisplayModeDescription.ExportToFile(dmd, ini_path)
|
||||
print(" Export OK: {} → {}".format(ok, ini_path))
|
||||
if ok and os.path.exists(ini_path):
|
||||
with open(ini_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
content = f.read()
|
||||
print(" ini-Inhalt ({} chars) — siehe Datei auf dem Desktop.".format(len(content)))
|
||||
# Falls Rhinos Log das Print durchlaesst, hier ueberhaupt rein
|
||||
print("===INI-START===")
|
||||
for line in content.split("\n"):
|
||||
print(line)
|
||||
print("===INI-END===")
|
||||
except Exception as ex:
|
||||
print(" Export-Fehler:", ex)
|
||||
@@ -0,0 +1,38 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""Hilfsscript: alle Dossier-Panel-Registrierungs-Flags clearen + Module
|
||||
neu laden. Nuetzlich nach Icon-/Layout-Aenderungen. ABER: Rhinos
|
||||
Panel-Manager cached die Icon-Bindung pro GUID — fuer NEUE Icons hilft
|
||||
oft nur ein kompletter Rhino-Neustart. Dieses Script ist fuer alles
|
||||
andere (Geometrie-/Bridge-Aenderungen).
|
||||
|
||||
Ausfuehrung in Rhino:
|
||||
_RunPythonScript "/Users/karim/STUDIO/DOSSIER/rhino/_reset_panels.py"
|
||||
"""
|
||||
import importlib, sys
|
||||
import scriptcontext as sc
|
||||
|
||||
_PANELS = ("elemente", "gestaltung", "oberleiste", "massstab", "ausschnitte",
|
||||
"layouts", "overrides", "werkzeuge", "dimensionen", "ebenen",
|
||||
"rhinopanel", "panel_base", "overrides_panel")
|
||||
|
||||
cleared = []
|
||||
for k in list(sc.sticky.keys()):
|
||||
kl = str(k).lower()
|
||||
if any(p in kl for p in _PANELS):
|
||||
cleared.append(k)
|
||||
sc.sticky[k] = None
|
||||
print("[reset] sticky cleared:", len(cleared))
|
||||
|
||||
reloaded = []
|
||||
for m in list(sys.modules):
|
||||
if any(p in m for p in _PANELS):
|
||||
try:
|
||||
importlib.reload(sys.modules[m])
|
||||
reloaded.append(m)
|
||||
except Exception as ex:
|
||||
print("[reset] reload {}: {}".format(m, ex))
|
||||
print("[reset] modules reloaded:", len(reloaded))
|
||||
print("[reset] FERTIG — Panels jetzt neu via _RunPythonScript oeffnen")
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
#
|
||||
# Nuke + Reset Rhino-8 Settings (Mac). Backupt vorher in einen Ordner mit
|
||||
# Zeitstempel — verlorene Settings koennen daraus rekonstruiert werden.
|
||||
#
|
||||
# Was geht VERLOREN:
|
||||
# - Alle Custom-Display-Modes (Dossier Plan, DOSSIER2D, etc.)
|
||||
# - Window-Layouts, Toolbar-Customizations
|
||||
# - Custom-Keyboard-Shortcuts
|
||||
# - Tab-Panel-Positions
|
||||
#
|
||||
# Was bleibt:
|
||||
# - Lizenz (License Manager Ordner wird NICHT angefasst)
|
||||
# - .3dm Templates
|
||||
# - Scripts unter scripts/
|
||||
# - Plugin-Einstellungen (in Plug-ins/-Unterordnern)
|
||||
#
|
||||
# Vorgehen:
|
||||
# 1. Rhino komplett quitten (Cmd+Q)
|
||||
# 2. ./rhino/_reset_rhino_settings.sh
|
||||
# 3. Rhino neu starten
|
||||
# 4. _RunPythonScript .../_reset_panels.py → Plan-Mode aus Template
|
||||
|
||||
set -e
|
||||
|
||||
SETTINGS_DIR="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings"
|
||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP="$SETTINGS_DIR.backup-$TIMESTAMP"
|
||||
|
||||
if [ ! -d "$SETTINGS_DIR" ]; then
|
||||
echo "[RESET] Settings-Ordner nicht gefunden: $SETTINGS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check ob Rhino läuft
|
||||
if pgrep -x "Rhinoceros" > /dev/null; then
|
||||
echo "[RESET] FEHLER: Rhino läuft noch. Bitte erst Cmd+Q drücken."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[RESET] Backup → $BACKUP"
|
||||
mv "$SETTINGS_DIR" "$BACKUP"
|
||||
echo "[RESET] Settings nun zurückgesetzt."
|
||||
echo "[RESET] Beim nächsten Rhino-Start werden Defaults regeneriert."
|
||||
echo "[RESET] Backup liegt unter: $BACKUP"
|
||||
echo ""
|
||||
echo "[RESET] Nächste Schritte:"
|
||||
echo " 1. Rhino starten"
|
||||
echo " 2. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_reset_panels.py"
|
||||
echo " → Dossier-Plan wird aus Template neu erstellt"
|
||||
@@ -0,0 +1,381 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
_startup_splash.py
|
||||
Petrol-grüner Splash-Screen waehrend des DOSSIER-Plugin-Startups.
|
||||
Borderless Eto-Form mit WebView + Inline-HTML im selben Stil wie der
|
||||
Launcher-Splash. Bedeckt visuell die 3+ Sekunden waehrend Rhino die
|
||||
Panels registriert + WindowLayout neu anwendet.
|
||||
|
||||
Wird von startup.py beim ersten Idle gezeigt und nach Layout-Apply
|
||||
(oder Timeout) wieder versteckt.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import Rhino
|
||||
import scriptcontext as sc
|
||||
|
||||
_SPLASH_KEY = "_dossier_startup_splash"
|
||||
_SPLASH_SHOWN_AT_KEY = "_dossier_startup_splash_shown_at"
|
||||
_SAFETY_TIMEOUT_SEC = 12.0 # spaetestens nach 12s wegmachen, falls Hide-Hook nicht feuert
|
||||
|
||||
# Marker den der Launcher direkt vor `open -a Rhinoceros` schreibt, damit
|
||||
# Plugin-Splash NICHT zusaetzlich zum Launcher-Splash erscheint.
|
||||
_OWNER_MARKER = os.path.expanduser(
|
||||
"~/Library/Application Support/ch.gabrielevarano.Dossier/splash_owner_launcher.flag"
|
||||
)
|
||||
_OWNER_FRESH_SEC = 30.0 # Stale-Schutz falls Launcher crasht
|
||||
|
||||
|
||||
_SPLASH_HTML = '''<!DOCTYPE html>
|
||||
<html lang="de"><head><meta charset="utf-8"/><title>Dossier laedt</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
:root {
|
||||
--accent: #5fa896; --accent-soft: #6fb5a3; --accent-deep: #2f5d54;
|
||||
--paper: #fff; --paper-mute: rgba(255,255,255,0.72); --paper-faint: rgba(255,255,255,0.45);
|
||||
--font-display: Krungthep, 'Archivo Black', sans-serif;
|
||||
--font-mono: 'DM Mono', 'Menlo', monospace;
|
||||
}
|
||||
html, body { margin:0; padding:0; width:100%; height:100%; background:transparent !important;
|
||||
color:var(--paper); overflow:hidden; font-family:var(--font-mono); user-select:none;
|
||||
-webkit-user-select:none; cursor:default; }
|
||||
.frame { box-sizing:border-box; width:100%; height:100%; padding:22px 26px;
|
||||
display:grid; grid-template-rows:auto 1fr auto; gap:0;
|
||||
background: radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
|
||||
border-radius:16px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.18); }
|
||||
.brand-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px; }
|
||||
.brand { font-family:var(--font-display); font-size:28px; letter-spacing:-0.01em;
|
||||
line-height:1; color:var(--paper); }
|
||||
.brand-dot { color:var(--accent-deep); }
|
||||
.version { font-family:var(--font-mono); font-size:10px; letter-spacing:0.10em;
|
||||
color:var(--paper-mute); text-transform:uppercase; }
|
||||
.status-row { align-self:end; display:flex; align-items:center; gap:10px;
|
||||
margin-top:18px; font-size:11px; letter-spacing:0.10em; color:var(--paper);
|
||||
text-transform:uppercase; }
|
||||
.dot-pulse { width:7px; height:7px; border-radius:50%; background:var(--paper); }
|
||||
.bar { position:relative; height:2px; width:100%; background:rgba(255,255,255,0.28);
|
||||
border-radius:2px; margin-top:12px; }
|
||||
.meta-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px;
|
||||
margin-top:10px; font-size:9px; letter-spacing:0.14em; color:var(--paper-faint);
|
||||
text-transform:uppercase; }
|
||||
</style></head><body>
|
||||
<div class="frame">
|
||||
<div class="brand-row">
|
||||
<div class="brand">DOSSIER<span class="brand-dot">.</span></div>
|
||||
<div class="version">Rhino 8 Plugin</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="status-row">
|
||||
<span class="dot-pulse"></span>
|
||||
<span>Plugin laedt — Panels werden platziert</span>
|
||||
</div>
|
||||
<div class="bar"></div>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span>AGPL-3.0 · Karim Gabriele Varano</span>
|
||||
<span>CPython 3.9</span>
|
||||
</div>
|
||||
</div></body></html>
|
||||
'''
|
||||
|
||||
|
||||
def _try_borderless_mac(form):
|
||||
"""Mac-spezifisch: direkter NSWindow-Zugriff via Eto.ControlObject um
|
||||
titlebar/Decorations komplett zu killen.
|
||||
|
||||
Eto.Mac.Forms.EtoWindow IST-A NSWindow (Xamarin.Mac-Subclass).
|
||||
StyleMask ist ein .NET-Enum-Property — Python.NET 3 verlangt explizite
|
||||
Enum-Konversion (kein impliziter int → Enum cast mehr). Wir leiten
|
||||
den Enum-Typ zur Laufzeit aus dem Getter ab und konstruieren den
|
||||
Borderless-Wert via System.Enum.ToObject."""
|
||||
nswindow = getattr(form, "ControlObject", None)
|
||||
if nswindow is None:
|
||||
print("[SPLASH] keine ControlObject auf Form")
|
||||
return False
|
||||
print("[SPLASH] ControlObject type:", str(type(nswindow)))
|
||||
|
||||
import System
|
||||
ok = False
|
||||
|
||||
# NSWindowStyleMaskBorderless = 0
|
||||
# NSWindowStyleMaskTitled = 1, FullSizeContentView = 32768
|
||||
try:
|
||||
current = nswindow.StyleMask
|
||||
style_type = type(current)
|
||||
borderless = System.Enum.ToObject(style_type, 0)
|
||||
nswindow.StyleMask = borderless
|
||||
print("[SPLASH] StyleMask=0 (Borderless) applied")
|
||||
ok = True
|
||||
except Exception as ex:
|
||||
print("[SPLASH] StyleMask Enum:", ex)
|
||||
# Fallback: FullSizeContentView (32768) + TitlebarAppearsTransparent
|
||||
# damit Content unter die (transparente) Titlebar reicht
|
||||
try:
|
||||
current = nswindow.StyleMask
|
||||
style_type = type(current)
|
||||
full = System.Enum.ToObject(style_type, 1 | 32768)
|
||||
nswindow.StyleMask = full
|
||||
print("[SPLASH] StyleMask=Titled|FullSize set (Fallback)")
|
||||
ok = True
|
||||
except Exception as ex2:
|
||||
print("[SPLASH] StyleMask Fallback:", ex2)
|
||||
|
||||
# Titlebar transparent + Titel unsichtbar
|
||||
def _set_prop(prop, value, log=False):
|
||||
try:
|
||||
setattr(nswindow, prop, value)
|
||||
if log: print("[SPLASH] {}={} OK".format(prop, value))
|
||||
return True
|
||||
except Exception as ex:
|
||||
if log: print("[SPLASH] {}:".format(prop), ex)
|
||||
return False
|
||||
|
||||
_set_prop("TitlebarAppearsTransparent", True, True)
|
||||
# NSWindowTitleHidden = 1
|
||||
try:
|
||||
tv_type = type(nswindow.TitleVisibility)
|
||||
nswindow.TitleVisibility = System.Enum.ToObject(tv_type, 1)
|
||||
print("[SPLASH] TitleVisibility=Hidden OK")
|
||||
except Exception as ex:
|
||||
print("[SPLASH] TitleVisibility:", ex)
|
||||
_set_prop("IsOpaque", False)
|
||||
_set_prop("HasShadow", True)
|
||||
_set_prop("MovableByWindowBackground", True)
|
||||
|
||||
# Clear NSWindow background damit rounded corners aus dem HTML sichtbar
|
||||
# werden. Xamarin.Mac exponiert NSColor.Clear als statische Property.
|
||||
try:
|
||||
from AppKit import NSColor as _NSC
|
||||
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
|
||||
if clear is not None:
|
||||
nswindow.BackgroundColor = clear
|
||||
print("[SPLASH] BackgroundColor=Clear OK")
|
||||
except Exception as ex:
|
||||
print("[SPLASH] BackgroundColor Clear:", ex)
|
||||
|
||||
# Force-Paint: Splash MUSS sichtbar sein BEVOR Rhino den Script-Thread
|
||||
# weiter belegt. Python-Script blockiert sonst die Main-Loop und der
|
||||
# Splash wuerde erst nach Script-Ende paintet werden — viel zu spaet.
|
||||
try: nswindow.OrderFrontRegardless()
|
||||
except Exception: pass
|
||||
try: nswindow.DisplayIfNeeded()
|
||||
except Exception: pass
|
||||
try: nswindow.Display()
|
||||
except Exception: pass
|
||||
|
||||
return ok
|
||||
|
||||
|
||||
def _try_transparent_webview_mac(wv):
|
||||
"""WKWebView transparent machen damit der NSWindow-Hintergrund (oder
|
||||
nichts) durchscheint und runde Ecken sichtbar werden. wv.ControlObject
|
||||
ist die WKWebView."""
|
||||
wk = getattr(wv, "ControlObject", None)
|
||||
if wk is None:
|
||||
print("[SPLASH] WebView: keine ControlObject"); return
|
||||
print("[SPLASH] WebView ControlObject type:", str(type(wk)))
|
||||
|
||||
# KVC: setValue:forKey:@"drawsBackground" → @NO. Funktioniert sowohl bei
|
||||
# WebView (alt) als auch WKWebView (NSObject KVC). Das ist der zuverlaessige
|
||||
# Weg WebView-Hintergrund komplett zu entfernen, besser als UnderPageBg.
|
||||
try:
|
||||
from Foundation import NSNumber, NSString
|
||||
try:
|
||||
wk.SetValueForKey(NSNumber.FromBoolean(False), NSString("drawsBackground"))
|
||||
print("[SPLASH] WebView drawsBackground=NO via KVC OK")
|
||||
except Exception as ex:
|
||||
print("[SPLASH] KVC drawsBackground:", ex)
|
||||
except Exception as ex:
|
||||
print("[SPLASH] Foundation import:", ex)
|
||||
|
||||
try:
|
||||
from AppKit import NSColor as _NSC
|
||||
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
|
||||
if clear is not None:
|
||||
try: wk.UnderPageBackgroundColor = clear
|
||||
except Exception: pass
|
||||
try:
|
||||
layer = getattr(wk, "Layer", None)
|
||||
if layer is not None:
|
||||
layer.BackgroundColor = clear.CGColor
|
||||
layer.Opaque = False
|
||||
print("[SPLASH] WebView Layer transparent OK")
|
||||
except Exception as ex:
|
||||
print("[SPLASH] WebView Layer:", ex)
|
||||
except Exception as ex:
|
||||
print("[SPLASH] WebView NSColor:", ex)
|
||||
|
||||
|
||||
def _dispatch_to_main(fn):
|
||||
"""Fuehrt fn beim naechsten Rhino-Idle-Event aus. Mac Eto/AppKit
|
||||
erfordert UI-Mutationen auf dem Main-Thread; threading.Timer-Callbacks
|
||||
laufen im falschen Thread und Close() crasht oder no-op't dort."""
|
||||
handler_ref = [None]
|
||||
def _idle(sender, e):
|
||||
try: Rhino.RhinoApp.Idle -= handler_ref[0]
|
||||
except Exception: pass
|
||||
try: fn()
|
||||
except Exception as ex:
|
||||
print("[SPLASH] dispatched fn:", ex)
|
||||
handler_ref[0] = _idle
|
||||
try: Rhino.RhinoApp.Idle += _idle
|
||||
except Exception as ex:
|
||||
print("[SPLASH] idle subscribe:", ex)
|
||||
try: fn()
|
||||
except Exception: pass
|
||||
|
||||
|
||||
def _install_safety_timeout():
|
||||
"""Registriert Idle-Handler der periodisch prueft ob _SAFETY_TIMEOUT_SEC
|
||||
erreicht ist. Cleanup-self wenn Splash bereits zu."""
|
||||
handler_ref = [None]
|
||||
def _idle(sender, e):
|
||||
try:
|
||||
if sc.sticky.get(_SPLASH_KEY) is None:
|
||||
try: Rhino.RhinoApp.Idle -= handler_ref[0]
|
||||
except Exception: pass
|
||||
return
|
||||
shown_at = sc.sticky.get(_SPLASH_SHOWN_AT_KEY) or 0
|
||||
if shown_at and (time.time() - shown_at) >= _SAFETY_TIMEOUT_SEC:
|
||||
try: Rhino.RhinoApp.Idle -= handler_ref[0]
|
||||
except Exception: pass
|
||||
print("[SPLASH] safety-timeout — auto-hide")
|
||||
try: _hide_main()
|
||||
except Exception: pass
|
||||
except Exception: pass
|
||||
handler_ref[0] = _idle
|
||||
try: Rhino.RhinoApp.Idle += _idle
|
||||
except Exception as ex:
|
||||
print("[SPLASH] safety install:", ex)
|
||||
|
||||
|
||||
def _launcher_owns_splash():
|
||||
"""True wenn Launcher direkt vor Rhino-Launch einen frischen Owner-
|
||||
Marker geschrieben hat. Verhindert doppelte Splashes."""
|
||||
try:
|
||||
if not os.path.isfile(_OWNER_MARKER):
|
||||
return False
|
||||
age = time.time() - os.path.getmtime(_OWNER_MARKER)
|
||||
if age <= _OWNER_FRESH_SEC:
|
||||
return True
|
||||
except Exception: pass
|
||||
return False
|
||||
|
||||
|
||||
def show():
|
||||
"""Zeigt den Splash. Idempotent — zweiter Aufruf bringt das bestehende
|
||||
Fenster nur in den Vordergrund. Auto-Hide nach _SAFETY_TIMEOUT_SEC
|
||||
als Fallback via Idle-Polling (NICHT threading.Timer — Mac UI braucht
|
||||
Main-Thread). Skipt wenn Launcher seinen eigenen Splash zeigt."""
|
||||
if _launcher_owns_splash():
|
||||
print("[SPLASH] Launcher zeigt eigenen Splash — skip"); return
|
||||
if sc.sticky.get(_SPLASH_KEY) is not None:
|
||||
print("[SPLASH] schon offen — skip"); return
|
||||
try:
|
||||
import Eto.Forms as ef
|
||||
import Eto.Drawing as ed
|
||||
except Exception as ex:
|
||||
print("[SPLASH] Eto-Import:", ex); return
|
||||
try:
|
||||
form = ef.Form()
|
||||
form.Title = "" # leerer Titel hilft bei Mac-Titlebar-Reduktion
|
||||
# Versuche WindowStyle.None (Eto-API, funktioniert nicht immer auf Mac)
|
||||
try: form.WindowStyle = getattr(ef.WindowStyle, "None")
|
||||
except Exception: pass
|
||||
# Alle Window-Chrome-Optionen aus
|
||||
for attr, val in (
|
||||
("Resizable", False), ("Minimizable", False),
|
||||
("Maximizable", False), ("Closeable", False),
|
||||
("ShowInTaskbar", False), ("Topmost", True),
|
||||
):
|
||||
try: setattr(form, attr, val)
|
||||
except Exception: pass
|
||||
try: form.Size = ed.Size(420, 160)
|
||||
except Exception: pass
|
||||
# Transparent so dass WebView's eigene rounded gradient sichtbar wird
|
||||
try:
|
||||
form.BackgroundColor = ed.Colors.Transparent
|
||||
except Exception:
|
||||
try: form.BackgroundColor = ed.Color(0.37, 0.66, 0.59)
|
||||
except Exception: pass
|
||||
|
||||
wv = ef.WebView()
|
||||
try:
|
||||
# WebView selber transparent damit das Form-Hintergrund durchscheint
|
||||
wv.BackgroundColor = ed.Colors.Transparent
|
||||
except Exception: pass
|
||||
try:
|
||||
wv.LoadHtml(_SPLASH_HTML)
|
||||
except Exception as ex:
|
||||
print("[SPLASH] LoadHtml:", ex)
|
||||
form.Content = wv
|
||||
|
||||
# Center on screen
|
||||
try:
|
||||
screen = ef.Screen.PrimaryScreen
|
||||
sb = screen.Bounds
|
||||
x = int(sb.X + (sb.Width - form.Size.Width) / 2)
|
||||
y = int(sb.Y + (sb.Height - form.Size.Height) / 2 - 100)
|
||||
form.Location = ed.Point(x, y)
|
||||
except Exception as ex:
|
||||
print("[SPLASH] center:", ex)
|
||||
|
||||
try: form.Show()
|
||||
except Exception as ex:
|
||||
print("[SPLASH] Show:", ex); return
|
||||
|
||||
# Mac-spezifischer Borderless-Hack — MUSS nach Show() laufen damit
|
||||
# die NSWindow existiert
|
||||
try:
|
||||
if _try_borderless_mac(form):
|
||||
print("[SPLASH] Borderless (Mac NSWindow) applied")
|
||||
except Exception as ex:
|
||||
print("[SPLASH] borderless-mac:", ex)
|
||||
# WebView transparent (rounded corners via HTML border-radius)
|
||||
try: _try_transparent_webview_mac(wv)
|
||||
except Exception as ex:
|
||||
print("[SPLASH] webview-clear:", ex)
|
||||
# Event-Loop einmal explizit pumpen damit Splash gepainted wird
|
||||
# bevor das Script weiter blockiert (sonst sieht Nutzer die Panels
|
||||
# zuerst entstehen und Splash erscheint erst danach).
|
||||
try:
|
||||
ef.Application.Instance.RunIteration()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sc.sticky[_SPLASH_KEY] = form
|
||||
sc.sticky[_SPLASH_SHOWN_AT_KEY] = time.time()
|
||||
print("[SPLASH] visible")
|
||||
# Safety-Timeout via Idle-Polling (Main-Thread, Mac-safe)
|
||||
_install_safety_timeout()
|
||||
except Exception as ex:
|
||||
print("[SPLASH] show:", ex)
|
||||
|
||||
|
||||
def _hide_main():
|
||||
"""Synchroner Close — MUSS auf Main-Thread laufen. Nur intern aufrufen,
|
||||
extern hide() verwenden."""
|
||||
form = sc.sticky.get(_SPLASH_KEY)
|
||||
if form is None:
|
||||
return
|
||||
sc.sticky[_SPLASH_KEY] = None
|
||||
sc.sticky[_SPLASH_SHOWN_AT_KEY] = None
|
||||
try: form.Close()
|
||||
except Exception:
|
||||
try: form.Visible = False
|
||||
except Exception as ex:
|
||||
print("[SPLASH] hide visible:", ex)
|
||||
print("[SPLASH] hidden")
|
||||
|
||||
|
||||
def hide():
|
||||
"""Versteckt + entsorgt den Splash. Idempotent + thread-safe —
|
||||
dispatcht auf Rhino-Main-Thread via Idle-Event."""
|
||||
if sc.sticky.get(_SPLASH_KEY) is None:
|
||||
return
|
||||
_dispatch_to_main(_hide_main)
|
||||
@@ -0,0 +1,26 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
about.py
|
||||
About-Dialog als Eto-Form + WebView. Vom DOSSIER-Logo-Klick in der
|
||||
Oberleiste geoeffnet. Read-only — keine Bridge-Endpoints noetig.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
if _HERE not in sys.path:
|
||||
sys.path.insert(0, _HERE)
|
||||
|
||||
import panel_base
|
||||
|
||||
|
||||
def open_as_window():
|
||||
"""Oeffnet das About-Fenster (Eto.Form + WebView). Read-only Info
|
||||
(Versionen + Lizenz) — Standard-SAVE/CANCEL-Bridge ohne Callbacks."""
|
||||
panel_base.open_satellite_window(
|
||||
"about",
|
||||
title="Über Dossier",
|
||||
size=(440, 380))
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'aussparung'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("aussparung")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'dach'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("dach")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'decke'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("decke")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Alias 'dkeys': oeffnet DOSSIER Shortcuts-Cheatsheet
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
import welcome
|
||||
welcome.show_cheatsheet()
|
||||
@@ -0,0 +1,8 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Alias 'dwelcome': zeigt DOSSIER Welcome-Screen manuell (force-mode,
|
||||
# ignoriert version-marker + optout)
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
import welcome
|
||||
welcome._show_welcome_now()
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'fenster'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("fenster")
|
||||
@@ -0,0 +1,436 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Pipette / Einstellungen-übernehmen: User klickt ein Source-Objekt, dessen
|
||||
# Attribute werden zur aktuellen Default-Einstellung gemacht — der naechste
|
||||
# gezeichnete Curve/Rectangle/etc. erbt sie automatisch.
|
||||
#
|
||||
# Was uebernommen wird:
|
||||
# 1. Layer → wird zum Current Layer
|
||||
# 2. Color (wenn per-Object Override) → wird Current Object-Color
|
||||
# 3. Linetype (per-Object) → Current
|
||||
# 4. PlotWeight (per-Object) → Current
|
||||
# 5. Fuer DOSSIER-Elemente (wand_axis, treppe_axis, etc.) → spezifische
|
||||
# UserStrings (Dicke, Modus, Breite, Stufen etc.) werden in sticky
|
||||
# gespeichert als _last_* → nachste Create-Wand/Treppe etc. nimmt sie.
|
||||
# 6. Bei Hatch-Quelle → wechselt auf den Curve dahinter (Hatch hat selten
|
||||
# direkt Sinn als Pipette-Quelle, eher der gefuellte Rahmen).
|
||||
import scriptcontext as sc
|
||||
import Rhino
|
||||
import Rhino.Input.Custom as ric
|
||||
import Rhino.DocObjects as rdoc
|
||||
from Rhino.Input import GetResult
|
||||
|
||||
|
||||
# Welche UserStrings pro DOSSIER-Type als sticky _last_* gespeichert werden,
|
||||
# damit das naechste Create-Cmd sie als Default uebernimmt.
|
||||
_DOSSIER_INHERIT = {
|
||||
"wand_axis": [
|
||||
("dossier_wand_dicke", "wand_dicke"),
|
||||
("dossier_wand_referenz", "wand_referenz"),
|
||||
("dossier_wand_modus", "wand_modus"),
|
||||
],
|
||||
"treppe_axis": [
|
||||
("dossier_treppe_breite", "treppe_breite"),
|
||||
("dossier_treppe_n", "treppe_n"),
|
||||
("dossier_treppe_referenz", "treppe_referenz"),
|
||||
("dossier_treppe_modus", "treppe_modus"),
|
||||
("dossier_treppe_lauf_d", "treppe_lauf_d"),
|
||||
("dossier_treppe_art", "treppe_art"),
|
||||
],
|
||||
"decke_outline": [
|
||||
("dossier_decke_dicke", "decke_dicke"),
|
||||
("dossier_decke_modus", "decke_modus"),
|
||||
],
|
||||
"dach_outline": [
|
||||
("dossier_dach_dicke", "dach_dicke"),
|
||||
("dossier_dach_neigung", "dach_neigung"),
|
||||
],
|
||||
"stuetze_point": [
|
||||
("dossier_trag_profil", "stuetze_profil"),
|
||||
("dossier_trag_b", "stuetze_b"),
|
||||
("dossier_trag_h", "stuetze_h"),
|
||||
],
|
||||
"traeger_axis": [
|
||||
("dossier_trag_profil", "traeger_profil"),
|
||||
("dossier_trag_b", "traeger_b"),
|
||||
("dossier_trag_h", "traeger_h"),
|
||||
],
|
||||
"oeffnung_point": [
|
||||
("dossier_oeff_breite", "oeff_breite"),
|
||||
("dossier_oeff_hoehe", "oeff_hoehe"),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _save_sticky(key, value):
|
||||
sc.sticky["elemente_last_" + key] = value
|
||||
|
||||
|
||||
def _find_curve_behind_hatch(doc, hatch_obj):
|
||||
"""Hatches haben in DOSSIER oft eine zugeordnete Source-Curve (gestaltung
|
||||
speichert die Curve-ID auf der Hatch via 'ebenen_fill_owner')."""
|
||||
try:
|
||||
owner = hatch_obj.Attributes.GetUserString("ebenen_fill_owner") or ""
|
||||
if owner:
|
||||
import System
|
||||
cid = System.Guid(owner)
|
||||
cobj = doc.Objects.FindId(cid)
|
||||
if cobj is not None and not cobj.IsDeleted: return cobj
|
||||
except Exception: pass
|
||||
return None
|
||||
|
||||
|
||||
def _run():
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
|
||||
go = ric.GetObject()
|
||||
go.SetCommandPrompt("Pipette: Quell-Objekt picken (Attribute uebernehmen)")
|
||||
go.GeometryFilter = (rdoc.ObjectType.Curve
|
||||
| rdoc.ObjectType.Brep
|
||||
| rdoc.ObjectType.Hatch
|
||||
| rdoc.ObjectType.PointSet
|
||||
| rdoc.ObjectType.Point
|
||||
| rdoc.ObjectType.Annotation
|
||||
| rdoc.ObjectType.TextDot)
|
||||
go.SubObjectSelect = False
|
||||
if go.Get() != GetResult.Object:
|
||||
print("[PIPETTE] abgebrochen"); return
|
||||
|
||||
src = go.Object(0).Object()
|
||||
if src is None: return
|
||||
|
||||
# Wenn Hatch gepickt, switch zur Source-Curve (gefuelltes Rechteck als
|
||||
# Pipette-Quelle ist intuitiver als die Hatch selbst)
|
||||
src_geom_type = type(src.Geometry).__name__
|
||||
if src_geom_type == "Hatch":
|
||||
cobj = _find_curve_behind_hatch(doc, src)
|
||||
if cobj is not None:
|
||||
src = cobj
|
||||
print("[PIPETTE] Hatch → zugeordnete Curve verwendet")
|
||||
|
||||
sa = src.Attributes
|
||||
msgs = []
|
||||
|
||||
# 1. Layer als Current setzen
|
||||
try:
|
||||
if doc.Layers.CurrentLayerIndex != sa.LayerIndex:
|
||||
doc.Layers.SetCurrentLayerIndex(sa.LayerIndex, True)
|
||||
try: lname = doc.Layers[sa.LayerIndex].FullPath
|
||||
except Exception: lname = "idx=" + str(sa.LayerIndex)
|
||||
msgs.append("Layer={}".format(lname))
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] Layer-Set:", ex)
|
||||
|
||||
# 2. Color
|
||||
try:
|
||||
cs = Rhino.ApplicationSettings.AppearanceSettings
|
||||
if sa.ColorSource == rdoc.ObjectColorSource.ColorFromObject:
|
||||
cs.DefaultObjectColorSource = rdoc.ObjectColorSource.ColorFromObject
|
||||
cs.DefaultObjectColor = sa.ObjectColor
|
||||
msgs.append("Color=obj")
|
||||
else:
|
||||
cs.DefaultObjectColorSource = rdoc.ObjectColorSource.ColorFromLayer
|
||||
msgs.append("Color=byLayer")
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] Color-Set:", ex)
|
||||
|
||||
# 3. Linetype + 4. PlotWeight — komplexer, weil Rhino keine direkten
|
||||
# AppearanceSettings dafuer hat. Wir ueberspringen bewusst, weil der
|
||||
# Layer-Wechsel die meisten Faelle abdeckt (Linetype + PlotWeight
|
||||
# kommen typisch ByLayer).
|
||||
|
||||
# 5. DOSSIER-spezifische Attrs in sticky uebernehmen
|
||||
try:
|
||||
dtype = sa.GetUserString("dossier_element_type") or ""
|
||||
if dtype in _DOSSIER_INHERIT:
|
||||
inherited = []
|
||||
for us_key, sticky_key in _DOSSIER_INHERIT[dtype]:
|
||||
v = sa.GetUserString(us_key)
|
||||
if v is None or v == "": continue
|
||||
# Numerische Werte ggf. konvertieren
|
||||
if any(k in sticky_key for k in ("dicke", "breite", "hoehe",
|
||||
"neigung", "lauf_d", "_b", "_h")):
|
||||
try: v = float(v)
|
||||
except Exception: pass
|
||||
elif "n" == sticky_key or sticky_key.endswith("_n"):
|
||||
try: v = int(float(v))
|
||||
except Exception: pass
|
||||
_save_sticky(sticky_key, v)
|
||||
inherited.append("{}={}".format(sticky_key, v))
|
||||
if inherited:
|
||||
msgs.append("DOSSIER " + dtype + ": " + ", ".join(inherited))
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] DOSSIER-Inherit:", ex)
|
||||
|
||||
if msgs:
|
||||
print("[PIPETTE] Uebernommen: " + " | ".join(msgs))
|
||||
else:
|
||||
print("[PIPETTE] Keine Aenderung (Source identisch zu Defaults)")
|
||||
|
||||
# 7. Per-Object Custom-Hatch / Custom-Attrs: speichern als "pending"
|
||||
# + one-shot Listener auf AddRhinoObject — wenn naechster Curve
|
||||
# gezeichnet ist, alle Custom-Attrs auf den uebertragen.
|
||||
_setup_pending_apply(doc, src)
|
||||
|
||||
# 6. Auto-Chain: passendes Draw-Command starten basierend auf
|
||||
# Source-Typ. So hat der User direkt "die richtige Tool in der Hand".
|
||||
_auto_chain(doc, src)
|
||||
|
||||
|
||||
def _capture_source_hatch_props(doc, src_obj):
|
||||
"""Wenn Source einen per-Object Custom-Hatch hat, sample dessen
|
||||
Properties (Pattern/Scale/Rotation/Color)."""
|
||||
try:
|
||||
sa = src_obj.Attributes
|
||||
fill_hid = sa.GetUserString("ebenen_fill_hatch_id") or ""
|
||||
if not fill_hid: return None
|
||||
import System
|
||||
hid = System.Guid(fill_hid)
|
||||
hobj = doc.Objects.FindId(hid)
|
||||
if hobj is None or hobj.IsDeleted: return None
|
||||
hg = hobj.Geometry
|
||||
ha = hobj.Attributes
|
||||
if not hasattr(hg, "PatternIndex"): return None
|
||||
return {
|
||||
"pattern_idx": int(hg.PatternIndex),
|
||||
"scale": float(hg.PatternScale),
|
||||
"rotation": float(hg.PatternRotation),
|
||||
"layer_idx": int(ha.LayerIndex),
|
||||
"color_source": int(ha.ColorSource),
|
||||
"color_argb": int(ha.ObjectColor.ToArgb()),
|
||||
"plot_color_source": int(ha.PlotColorSource),
|
||||
"plot_color_argb": int(ha.PlotColor.ToArgb()),
|
||||
"linetype_source": int(ha.LinetypeSource),
|
||||
"linetype_idx": int(ha.LinetypeIndex),
|
||||
}
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] capture-hatch:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def _setup_pending_apply(doc, src_obj):
|
||||
"""Speichert Source-Custom-Attrs in sticky + registriert one-shot
|
||||
AddRhinoObject-Listener der die Attrs (inkl. Hatch) auf den naechsten
|
||||
neuen Curve uebertraegt. Nach Apply wird Listener wieder entfernt."""
|
||||
sa = src_obj.Attributes
|
||||
# Custom-User-Strings sammeln (DOSSIER-Element-Typen + andere). Skip
|
||||
# die Fill-Tracking-Keys weil wir den Hatch neu erstellen mit neuer ID.
|
||||
skip_keys = {
|
||||
"ebenen_fill_hatch_id", # zeigt auf alte Source-Hatch-ID
|
||||
"ebenen_fill_owner",
|
||||
}
|
||||
user_strings = {}
|
||||
try:
|
||||
for k in sa.GetUserStringKeys():
|
||||
if k in skip_keys: continue
|
||||
v = sa.GetUserString(k)
|
||||
if v is not None: user_strings[k] = v
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] user-strings:", ex)
|
||||
|
||||
# Source-Geometrie Closed-State erfassen — wenn Source closed war,
|
||||
# erzwingen wir nach dem Add auch auf der Kopie ein Close (Polyline
|
||||
# bleibt sonst standardmaessig offen, hatten User-Feedback dazu).
|
||||
src_closed = False
|
||||
try:
|
||||
import Rhino.Geometry as _rg
|
||||
sg = src_obj.Geometry
|
||||
if isinstance(sg, _rg.Curve) and sg.IsClosed:
|
||||
src_closed = True
|
||||
except Exception: pass
|
||||
|
||||
pending = {
|
||||
"linetype_source": int(sa.LinetypeSource),
|
||||
"linetype_idx": int(sa.LinetypeIndex),
|
||||
"plot_weight_source": int(sa.PlotWeightSource),
|
||||
"plot_weight": float(sa.PlotWeight),
|
||||
"user_strings": user_strings,
|
||||
"hatch_props": _capture_source_hatch_props(doc, src_obj),
|
||||
"src_closed": src_closed,
|
||||
}
|
||||
sc.sticky["dossier_pipette_pending"] = pending
|
||||
|
||||
# One-shot handler — applied beim naechsten AddRhinoObject + entfernt sich
|
||||
def _on_add(sender, e):
|
||||
try:
|
||||
obj = e.TheObject
|
||||
if obj is None or obj.IsDeleted: return
|
||||
import Rhino.Geometry as rg2
|
||||
if not isinstance(obj.Geometry, rg2.Curve): return
|
||||
_apply_pending(doc, obj, pending)
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] one-shot apply:", ex)
|
||||
finally:
|
||||
try: Rhino.RhinoDoc.AddRhinoObject -= _on_add
|
||||
except Exception: pass
|
||||
sc.sticky.pop("dossier_pipette_pending", None)
|
||||
|
||||
try:
|
||||
Rhino.RhinoDoc.AddRhinoObject += _on_add
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] listener-install:", ex)
|
||||
|
||||
|
||||
def _force_close_curve(crv):
|
||||
"""Schliesst eine offene Polyline durch Anhaengen des Startpunkts.
|
||||
Generische Curves: MakeClosed (nur wenn Endpunkte nahe) oder Join mit
|
||||
Lueckensegment. Returns geschlossene Curve oder None bei Fehler."""
|
||||
import Rhino.Geometry as rg2
|
||||
if crv is None or crv.IsClosed: return None
|
||||
try:
|
||||
if isinstance(crv, rg2.PolylineCurve):
|
||||
ok, pl = crv.TryGetPolyline()
|
||||
if ok and pl is not None and pl.Count >= 2:
|
||||
if pl[0].DistanceTo(pl[pl.Count - 1]) > 1e-9:
|
||||
pl.Add(pl[0])
|
||||
return rg2.PolylineCurve(pl)
|
||||
return None
|
||||
# Generic: erst MakeClosed (closed wenn Endpunkte innerhalb tol)
|
||||
try:
|
||||
if crv.MakeClosed(1e-6): return crv
|
||||
except Exception: pass
|
||||
# Fallback: Lueckensegment einfuegen + joinen
|
||||
line = rg2.LineCurve(crv.PointAtEnd, crv.PointAtStart)
|
||||
joined = rg2.Curve.JoinCurves([crv, line], 1e-6)
|
||||
if joined and len(joined) > 0 and joined[0].IsClosed:
|
||||
return joined[0]
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] force-close:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def _apply_pending(doc, new_obj, pending):
|
||||
"""Wendet pending state auf das neu erzeugte Objekt an."""
|
||||
import Rhino.Geometry as rg2
|
||||
import System
|
||||
# Close-Erzwingen wenn Source geschlossen war — Polyline-Command erzeugt
|
||||
# standardmaessig offene Curves; Pipette soll den Closed-State erhalten.
|
||||
if pending.get("src_closed"):
|
||||
try:
|
||||
crv = new_obj.Geometry
|
||||
if isinstance(crv, rg2.Curve) and not crv.IsClosed:
|
||||
closed = _force_close_curve(crv)
|
||||
if closed is not None:
|
||||
if doc.Objects.Replace(new_obj.Id, closed):
|
||||
ref = doc.Objects.FindId(new_obj.Id)
|
||||
if ref is not None: new_obj = ref
|
||||
print("[PIPETTE] Polyline auto-geschlossen (Source war closed)")
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] close-replace:", ex)
|
||||
# Linetype + PlotWeight overrides
|
||||
try:
|
||||
na = new_obj.Attributes.Duplicate()
|
||||
if pending["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
|
||||
na.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
|
||||
na.LinetypeIndex = pending["linetype_idx"]
|
||||
if pending["plot_weight_source"] == int(rdoc.ObjectPlotWeightSource.PlotWeightFromObject):
|
||||
na.PlotWeightSource = rdoc.ObjectPlotWeightSource.PlotWeightFromObject
|
||||
na.PlotWeight = pending["plot_weight"]
|
||||
# UserStrings 1:1 kopieren
|
||||
for k, v in pending["user_strings"].items():
|
||||
try: na.SetUserString(k, v)
|
||||
except Exception: pass
|
||||
doc.Objects.ModifyAttributes(new_obj, na, True)
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] apply-attrs:", ex)
|
||||
|
||||
# Per-Object Custom-Hatch: nachbauen wenn Source einen hatte UND
|
||||
# der neue Curve closed ist
|
||||
hp = pending.get("hatch_props")
|
||||
if hp is None: return
|
||||
try:
|
||||
crv = new_obj.Geometry
|
||||
if not isinstance(crv, rg2.Curve) or not crv.IsClosed: return
|
||||
tol = doc.ModelAbsoluteTolerance
|
||||
hatches = rg2.Hatch.Create(crv, hp["pattern_idx"],
|
||||
hp["rotation"], hp["scale"], tol)
|
||||
if not hatches or len(hatches) == 0: return
|
||||
ha = rdoc.ObjectAttributes()
|
||||
ha.LayerIndex = hp["layer_idx"]
|
||||
ha.ColorSource = rdoc.ObjectColorSource(hp["color_source"])
|
||||
ha.ObjectColor = System.Drawing.Color.FromArgb(hp["color_argb"])
|
||||
try:
|
||||
ha.PlotColorSource = rdoc.ObjectPlotColorSource(hp["plot_color_source"])
|
||||
ha.PlotColor = System.Drawing.Color.FromArgb(hp["plot_color_argb"])
|
||||
except Exception: pass
|
||||
if hp["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
|
||||
ha.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
|
||||
ha.LinetypeIndex = hp["linetype_idx"]
|
||||
ha.SetUserString("ebenen_fill_source", "object")
|
||||
ha.SetUserString("ebenen_fill_owner", str(new_obj.Id))
|
||||
new_hid = doc.Objects.AddHatch(hatches[0], ha)
|
||||
if new_hid and new_hid != System.Guid.Empty:
|
||||
# Cross-Link: Curve speichert Hatch-ID
|
||||
ca = new_obj.Attributes.Duplicate()
|
||||
ca.SetUserString("ebenen_fill_hatch_id", str(new_hid))
|
||||
ca.SetUserString("ebenen_fill_source", "object")
|
||||
doc.Objects.ModifyAttributes(new_obj, ca, True)
|
||||
print("[PIPETTE] Per-Object Hatch uebernommen (Pattern={}, Scale={})"
|
||||
.format(hp["pattern_idx"], hp["scale"]))
|
||||
except Exception as ex:
|
||||
print("[PIPETTE] hatch-replicate:", ex)
|
||||
|
||||
|
||||
def _auto_chain(doc, src):
|
||||
"""Startet das passende Draw-Command basierend auf Source-Typ."""
|
||||
sa = src.Attributes
|
||||
dtype = sa.GetUserString("dossier_element_type") or ""
|
||||
geom = src.Geometry
|
||||
geom_type = type(geom).__name__
|
||||
|
||||
# DOSSIER-BIM: triggere den Dispatcher
|
||||
_DOSSIER_DRAW = {
|
||||
"wand_axis": "wand",
|
||||
"treppe_axis": "treppe",
|
||||
"decke_outline": "decke",
|
||||
"dach_outline": "dach",
|
||||
"stuetze_point": "stuetze",
|
||||
"traeger_axis": "traeger",
|
||||
"oeffnung_point": None, # braucht parent-Wand-Kontext → skip auto-chain
|
||||
"raum_outline": "raum",
|
||||
}
|
||||
if dtype in _DOSSIER_DRAW:
|
||||
action = _DOSSIER_DRAW[dtype]
|
||||
if action:
|
||||
import os
|
||||
_here = os.path.dirname(os.path.abspath(__file__))
|
||||
wrapper = os.path.join(_here, action + ".py")
|
||||
if os.path.exists(wrapper):
|
||||
Rhino.RhinoApp.RunScript(
|
||||
'_-RunPythonScript "{}"'.format(wrapper), False)
|
||||
print("[PIPETTE] → starte DOSSIER {}".format(action))
|
||||
return
|
||||
|
||||
# Standard-Rhino-Curves: detect Typ → entsprechendes Draw-Cmd
|
||||
cmd = None
|
||||
if geom_type == "LineCurve":
|
||||
cmd = "_Line"
|
||||
elif geom_type == "ArcCurve":
|
||||
# ArcCurve mit voller Sweep = Kreis
|
||||
try:
|
||||
if geom.IsClosed: cmd = "_Circle"
|
||||
else: cmd = "_Arc"
|
||||
except Exception:
|
||||
cmd = "_Arc"
|
||||
elif geom_type == "PolylineCurve":
|
||||
try:
|
||||
ok, pl = geom.TryGetPolyline()
|
||||
if ok and pl is not None and pl.IsClosed and pl.Count == 5:
|
||||
# Geschlossen + 4 Segmente → vermutlich Rectangle
|
||||
cmd = "_Rectangle"
|
||||
else:
|
||||
cmd = "_Polyline"
|
||||
except Exception:
|
||||
cmd = "_Polyline"
|
||||
elif geom_type == "NurbsCurve":
|
||||
cmd = "_Curve"
|
||||
elif geom_type == "TextEntity":
|
||||
cmd = "_Text"
|
||||
|
||||
if cmd:
|
||||
Rhino.RhinoApp.RunScript(cmd, False)
|
||||
print("[PIPETTE] → starte {}".format(cmd))
|
||||
|
||||
|
||||
_run()
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'raum'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("raum")
|
||||
@@ -0,0 +1,57 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Wrapper fuer dSection: interaktiver Schnitt-Pick (2 Punkte + Blickrichtung).
|
||||
# Defaults kommen aus Project-Settings.defaults; nach erfolgreicher
|
||||
# Erstellung wird der neue Schnitt als aktive Zeichnungs-Ebene set.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
import Rhino
|
||||
import scriptcontext as sc
|
||||
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None:
|
||||
print("[SECTION] kein aktives Dokument")
|
||||
else:
|
||||
try:
|
||||
import schnitte
|
||||
# Defaults aus Project-Settings; Fallback auf hartkodierte Werte.
|
||||
defaults = {
|
||||
"depthBack": 8.0, "heightMin": -1.0, "heightMax": 12.0,
|
||||
"cutAtLine": True, "namePrefix": "S",
|
||||
}
|
||||
try:
|
||||
import layers_panel as rhinopanel
|
||||
ps = rhinopanel.load_project_settings(doc)
|
||||
d = (ps or {}).get("defaults", {})
|
||||
defaults["depthBack"] = float(d.get("schnittDepthBack", 8.0))
|
||||
defaults["heightMin"] = float(d.get("schnittHeightMin", -1.0))
|
||||
defaults["heightMax"] = float(d.get("schnittHeightMax", 12.0))
|
||||
except Exception as ex:
|
||||
print("[SECTION] defaults from project-settings:", ex)
|
||||
|
||||
sid = schnitte.pick_schnitt_interactive(doc, defaults=defaults)
|
||||
if not sid:
|
||||
print("[SECTION] abgebrochen")
|
||||
else:
|
||||
# Broadcast neue Zeichnungs-Ebene an Panels + auto-aktivieren
|
||||
try:
|
||||
eb = sc.sticky.get("ebenen_bridge")
|
||||
if eb is not None:
|
||||
eb._send_state()
|
||||
except Exception as ex:
|
||||
print("[SECTION] broadcast:", ex)
|
||||
try:
|
||||
import json
|
||||
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]"
|
||||
z_list = json.loads(zraw)
|
||||
new_z = next((x for x in z_list
|
||||
if isinstance(x, dict) and x.get("id") == sid), None)
|
||||
if new_z is not None:
|
||||
eb = sc.sticky.get("ebenen_bridge")
|
||||
if eb is not None:
|
||||
eb._set_active_zeichnungsebene(new_z)
|
||||
print("[SECTION] erstellt: {}".format(sid))
|
||||
except Exception as ex:
|
||||
print("[SECTION] auto-activate:", ex)
|
||||
except Exception as ex:
|
||||
print("[SECTION] error:", ex)
|
||||
@@ -0,0 +1,458 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Smart-Join: bei geschlossenen Curves → BooleanUnion (innere Linien weg),
|
||||
# bei offenen Curves → normales _Join (Endpunkt-Verbindung).
|
||||
# Sicherheits-Filter:
|
||||
# A) Group by Layer + Object-Overrides (Color/Linetype/PlotWeight) + Fill —
|
||||
# nur Curves mit IDENTISCHEN visuellen Attributen werden gemerged.
|
||||
# C) Pre-Check Overlap — BooleanUnion liefert genauso viele Outputs wie
|
||||
# Inputs wenn nichts overlapt → dann KEINE Aktion, Curves bleiben.
|
||||
# Kombinierter Effekt: nur visuell zusammengehoerige UND tatsaechlich
|
||||
# ueberlappende Curves werden zu einer Outline vereint.
|
||||
import scriptcontext as sc
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
import Rhino.DocObjects as rdoc
|
||||
|
||||
|
||||
def _attr_key(obj):
|
||||
"""Tuple das definiert ob 2 Curves visuell identisch sind. Layer +
|
||||
Per-Object-Overrides (alles was ByObject nicht ByLayer ist) + Fill-
|
||||
State (Hatch-ID + No-Fill-Flag)."""
|
||||
a = obj.Attributes
|
||||
layer_idx = a.LayerIndex
|
||||
|
||||
# Color: nur Object-Override unterscheidend, ByLayer ist gleich.
|
||||
col_key = ("layer",)
|
||||
try:
|
||||
if a.ColorSource == rdoc.ObjectColorSource.ColorFromObject:
|
||||
col_key = ("obj", a.ObjectColor.ToArgb())
|
||||
except Exception: pass
|
||||
|
||||
# Linetype
|
||||
lt_key = ("layer",)
|
||||
try:
|
||||
if a.LinetypeSource == rdoc.ObjectLinetypeSource.LinetypeFromObject:
|
||||
lt_key = ("obj", a.LinetypeIndex)
|
||||
except Exception: pass
|
||||
|
||||
# PlotWeight
|
||||
pw_key = ("layer",)
|
||||
try:
|
||||
if a.PlotWeightSource == rdoc.ObjectPlotWeightSource.PlotWeightFromObject:
|
||||
pw_key = ("obj", float(a.PlotWeight))
|
||||
except Exception: pass
|
||||
|
||||
# Fill / Hatch via gestaltung-UserStrings
|
||||
fill_hatch = ""
|
||||
fill_source = ""
|
||||
no_fill = ""
|
||||
try:
|
||||
fill_hatch = a.GetUserString("ebenen_fill_hatch_id") or ""
|
||||
fill_source = a.GetUserString("ebenen_fill_source") or ""
|
||||
no_fill = a.GetUserString("ebenen_no_fill") or ""
|
||||
except Exception: pass
|
||||
# Fuer Gruppierung zaehlt: "hatte Fill ja/nein" + Quelle + No-Fill-Flag.
|
||||
fill_key = (bool(fill_hatch), fill_source, no_fill)
|
||||
|
||||
return (layer_idx, col_key, lt_key, pw_key, fill_key)
|
||||
|
||||
|
||||
def _replace_curve_endpoint(curve, which_end, new_pt):
|
||||
"""Ersetze Start- (which_end=0) oder End-Punkt (which_end=1). Liefert
|
||||
eine neue Curve oder None bei nicht-unterstuetztem Typ."""
|
||||
if isinstance(curve, rg.LineCurve):
|
||||
if which_end == 0:
|
||||
return rg.LineCurve(new_pt, curve.PointAtEnd)
|
||||
return rg.LineCurve(curve.PointAtStart, new_pt)
|
||||
if isinstance(curve, rg.PolylineCurve):
|
||||
n = curve.PointCount
|
||||
pts = [curve.Point(i) for i in range(n)]
|
||||
if which_end == 0: pts[0] = new_pt
|
||||
else: pts[-1] = new_pt
|
||||
return rg.PolylineCurve(pts)
|
||||
# Fallback: generische Curve via Extend
|
||||
cu = curve.DuplicateCurve()
|
||||
if cu is None: return None
|
||||
end_enum = rg.CurveEnd.Start if which_end == 0 else rg.CurveEnd.End
|
||||
try:
|
||||
return cu.Extend(end_enum,
|
||||
rg.CurveExtensionStyle.Line,
|
||||
[rg.Point3d(new_pt)])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _walls_and_curves_from_sel(doc, sel):
|
||||
"""Liefert (axes, generic_curves). Axes = dedup Wand-Achsen (per wall_id),
|
||||
generic_curves = offene Kurven die KEINE Wand sind. wand_volumes werden
|
||||
auf ihre Achse via wall_id resolved (auto-group bringt axis+volume
|
||||
automatisch beide in sel)."""
|
||||
seen_walls = set()
|
||||
axes = []
|
||||
generic = []
|
||||
# Pre-Index wand_axis by wall_id fuer schnelles Lookup
|
||||
axis_by_id = {}
|
||||
for o in doc.Objects:
|
||||
if o.Attributes.GetUserString("dossier_element_type") == "wand_axis":
|
||||
wid = o.Attributes.GetUserString("dossier_element_id") or ""
|
||||
if wid: axis_by_id[wid] = o
|
||||
for obj in sel:
|
||||
t = obj.Attributes.GetUserString("dossier_element_type") or ""
|
||||
wid = obj.Attributes.GetUserString("dossier_element_id") or ""
|
||||
if t == "wand_axis" and wid and wid not in seen_walls:
|
||||
axes.append(obj); seen_walls.add(wid)
|
||||
elif t == "wand_volume" and wid:
|
||||
wall_ids = {wid}
|
||||
members_raw = obj.Attributes.GetUserString(
|
||||
"dossier_wand_chain_members") or ""
|
||||
if members_raw:
|
||||
try:
|
||||
import json as _j
|
||||
for c in _j.loads(members_raw):
|
||||
if c: wall_ids.add(c)
|
||||
except Exception: pass
|
||||
for w in wall_ids:
|
||||
if w in seen_walls: continue
|
||||
ax = axis_by_id.get(w)
|
||||
if ax is not None:
|
||||
axes.append(ax); seen_walls.add(w)
|
||||
elif t == "":
|
||||
g = obj.Geometry
|
||||
if isinstance(g, rg.Curve) and not g.IsClosed:
|
||||
generic.append(obj)
|
||||
return axes, generic
|
||||
|
||||
|
||||
def _find_nearest_other_wand_axis(doc, my_axis_obj, my_id, tol=1.0):
|
||||
"""Findet die naechste andere wand_axis im Doc (innerhalb tol).
|
||||
Return das axis Object oder None."""
|
||||
if my_axis_obj is None: return None
|
||||
g = my_axis_obj.Geometry
|
||||
if not isinstance(g, rg.Curve): return None
|
||||
bb = g.GetBoundingBox(True)
|
||||
if not bb.IsValid: return None
|
||||
best = None; best_d = tol
|
||||
for obj in doc.Objects:
|
||||
try:
|
||||
if obj.Attributes.GetUserString("dossier_element_type") != "wand_axis":
|
||||
continue
|
||||
wid = obj.Attributes.GetUserString("dossier_element_id") or ""
|
||||
if wid == my_id or not wid: continue
|
||||
except Exception: continue
|
||||
og = obj.Geometry
|
||||
if not isinstance(og, rg.Curve): continue
|
||||
try:
|
||||
# Mindest-Distanz: Endpunkte gegeneinander UND ClosestPoint
|
||||
d_min = float('inf')
|
||||
for ep in (g.PointAtStart, g.PointAtEnd):
|
||||
rc, t = og.ClosestPoint(ep)
|
||||
if rc:
|
||||
d = og.PointAt(t).DistanceTo(ep)
|
||||
if d < d_min: d_min = d
|
||||
for ep in (og.PointAtStart, og.PointAtEnd):
|
||||
rc, t = g.ClosestPoint(ep)
|
||||
if rc:
|
||||
d = g.PointAt(t).DistanceTo(ep)
|
||||
if d < d_min: d_min = d
|
||||
if d_min < best_d:
|
||||
best_d = d_min; best = obj
|
||||
except Exception: continue
|
||||
return best
|
||||
|
||||
|
||||
def _t_join_attempt(doc, sel):
|
||||
"""T-Join: 2 OFFENE Kurven wobei der EINE Endpunkt der einen Kurve
|
||||
nahe (< 1m) auf der ANDEREN Kurve mitten landet (zwischen deren
|
||||
Endpunkten). Schiebt diesen Endpunkt exakt auf die andere Kurve.
|
||||
Die andere Kurve bleibt unchanged.
|
||||
|
||||
Auch 1-Wand-Modus: wenn nur 1 wand_axis selektiert, sucht automatisch
|
||||
die naechste andere Wand und snappt diese eine.
|
||||
|
||||
Liefert True wenn ausgefuehrt."""
|
||||
axes, generic = _walls_and_curves_from_sel(doc, sel)
|
||||
if len(axes) == 2 and len(generic) == 0:
|
||||
o1, o2 = axes[0], axes[1]
|
||||
elif len(axes) == 1 and len(generic) == 0:
|
||||
# 1-Wand-Modus: finde naechste andere wand_axis im Doc
|
||||
my_id = axes[0].Attributes.GetUserString("dossier_element_id") or ""
|
||||
other = _find_nearest_other_wand_axis(doc, axes[0], my_id, tol=1.0)
|
||||
if other is None:
|
||||
print("[SMART-JOIN] 1-Wand T-Join: keine Nachbar-Wand "
|
||||
"innerhalb 1m gefunden")
|
||||
return False
|
||||
o1 = axes[0]; o2 = other
|
||||
print("[SMART-JOIN] 1-Wand T-Join: snappe an Nachbar-Wand")
|
||||
elif len(axes) == 0 and len(generic) == 2:
|
||||
o1, o2 = generic[0], generic[1]
|
||||
else:
|
||||
return False
|
||||
c1 = o1.Geometry; c2 = o2.Geometry
|
||||
if not (isinstance(c1, rg.Curve) and isinstance(c2, rg.Curve)):
|
||||
return False
|
||||
if c1.IsClosed or c2.IsClosed: return False
|
||||
tol_snap = 1.00 # 1 m Snap-Radius fuer T-Verbindung — generous damit
|
||||
# auch grosse Drift (z.B. wenn User auf Outline statt
|
||||
# Axis gesnappt hat = 30cm Wand-dicke) gefangen wird
|
||||
end_tol = 0.05 # 5cm: wenn closest-point nahe Endpunkt → eigentlich L
|
||||
candidates = []
|
||||
debug_rows = []
|
||||
# Pro Endpunkt der einen Kurve: ClosestPoint auf der ANDEREN Kurve
|
||||
for (a_obj, ac, b_obj, bc) in ((o1, c1, o2, c2), (o2, c2, o1, c1)):
|
||||
for end in (0, 1):
|
||||
ep = ac.PointAtStart if end == 0 else ac.PointAtEnd
|
||||
try:
|
||||
rc, t = bc.ClosestPoint(ep)
|
||||
if not rc:
|
||||
debug_rows.append(("axis_end={}".format(end), "ClosestPoint failed"))
|
||||
continue
|
||||
cp = bc.PointAt(t)
|
||||
d = cp.DistanceTo(ep)
|
||||
ps = bc.PointAtStart; pe = bc.PointAtEnd
|
||||
d_to_ps = cp.DistanceTo(ps)
|
||||
d_to_pe = cp.DistanceTo(pe)
|
||||
reason = None
|
||||
if d < 1e-6: reason = "schon gesnappt"
|
||||
elif d > tol_snap: reason = "zu weit ({:.3f} > {:.2f})".format(d, tol_snap)
|
||||
elif d_to_ps < end_tol: reason = "cp nahe Endpunkt-Start ({:.3f}<{:.2f}) → L-Join Sache".format(d_to_ps, end_tol)
|
||||
elif d_to_pe < end_tol: reason = "cp nahe Endpunkt-End ({:.3f}<{:.2f}) → L-Join Sache".format(d_to_pe, end_tol)
|
||||
debug_rows.append(("axis_end={} d={:.3f}".format(end, d),
|
||||
reason or "candidate"))
|
||||
if reason is None:
|
||||
candidates.append((d, a_obj, ac, end, cp))
|
||||
except Exception as ex:
|
||||
debug_rows.append(("axis_end={}".format(end), "exc: {}".format(ex)))
|
||||
# Diagnostic alles ausgeben
|
||||
for tag, msg in debug_rows:
|
||||
print("[SMART-JOIN] T-Join check {}: {}".format(tag, msg))
|
||||
if not candidates: return False
|
||||
# Naechster Endpunkt → der wird gesnappt
|
||||
candidates.sort(key=lambda x: x[0])
|
||||
_d, a_obj, ac, end, cp = candidates[0]
|
||||
new_c = _replace_curve_endpoint(ac, end, cp)
|
||||
if new_c is None: return False
|
||||
ur = doc.BeginUndoRecord("DOSSIER T-Join")
|
||||
try:
|
||||
ok = doc.Objects.Replace(a_obj.Id, new_c)
|
||||
return bool(ok)
|
||||
finally:
|
||||
doc.EndUndoRecord(ur)
|
||||
|
||||
|
||||
def _l_join_attempt(doc, sel):
|
||||
"""Wenn genau 2 OFFENE Kurven (Wand-Achsen oder generische Lines)
|
||||
selektiert sind, deren End-Tangenten sich in einem Punkt schneiden →
|
||||
beide Kurven extend/shorten zu diesem Punkt (= L-Form). True wenn
|
||||
ausgefuehrt."""
|
||||
axes, generic = _walls_and_curves_from_sel(doc, sel)
|
||||
# Erlaubte Konfigs: 2 Wand-Achsen ODER 2 generische Kurven (keine mix)
|
||||
if len(axes) == 2 and len(generic) == 0:
|
||||
o1, o2 = axes[0], axes[1]
|
||||
elif len(axes) == 0 and len(generic) == 2:
|
||||
o1, o2 = generic[0], generic[1]
|
||||
else:
|
||||
return False
|
||||
c1 = o1.Geometry; c2 = o2.Geometry
|
||||
if not (isinstance(c1, rg.Curve) and isinstance(c2, rg.Curve)):
|
||||
return False
|
||||
if c1.IsClosed or c2.IsClosed: return False
|
||||
tol = max(doc.ModelAbsoluteTolerance, 1e-6)
|
||||
# Closest endpoint pair (a_end, b_end ∈ {0=start, 1=end})
|
||||
pairs = [
|
||||
(c1.PointAtStart, c2.PointAtStart, 0, 0),
|
||||
(c1.PointAtStart, c2.PointAtEnd, 0, 1),
|
||||
(c1.PointAtEnd, c2.PointAtStart, 1, 0),
|
||||
(c1.PointAtEnd, c2.PointAtEnd, 1, 1),
|
||||
]
|
||||
pairs.sort(key=lambda p: p[0].DistanceTo(p[1]))
|
||||
p1, p2, e1, e2 = pairs[0]
|
||||
if p1.DistanceTo(p2) < tol:
|
||||
return False # bereits verbunden
|
||||
def _out_dir(c, end):
|
||||
return -c.TangentAtStart if end == 0 else c.TangentAtEnd
|
||||
d1 = _out_dir(c1, e1)
|
||||
d2 = _out_dir(c2, e2)
|
||||
# Parallel-Check (Cross-Produkt-Laenge in XY)
|
||||
cross_z = d1.X * d2.Y - d1.Y * d2.X
|
||||
if abs(cross_z) < 1e-9: return False # parallel
|
||||
# Unendliche Linien-Intersection
|
||||
line1 = rg.Line(p1, p1 + d1)
|
||||
line2 = rg.Line(p2, p2 + d2)
|
||||
rc, t_a, t_b = rg.Intersect.Intersection.LineLine(line1, line2, tol, False)
|
||||
if not rc: return False
|
||||
ipt = line1.PointAt(t_a)
|
||||
if line2.PointAt(t_b).DistanceTo(ipt) > 0.01:
|
||||
return False # Schiefe Linien in 3D
|
||||
nc1 = _replace_curve_endpoint(c1, e1, ipt)
|
||||
nc2 = _replace_curve_endpoint(c2, e2, ipt)
|
||||
if nc1 is None or nc2 is None: return False
|
||||
ur = doc.BeginUndoRecord("DOSSIER L-Join")
|
||||
try:
|
||||
ok1 = doc.Objects.Replace(o1.Id, nc1)
|
||||
ok2 = doc.Objects.Replace(o2.Id, nc2)
|
||||
return bool(ok1 and ok2)
|
||||
finally:
|
||||
doc.EndUndoRecord(ur)
|
||||
|
||||
|
||||
def _run():
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
sel = list(doc.Objects.GetSelectedObjects(False, False))
|
||||
if not sel:
|
||||
Rhino.RhinoApp.RunScript("_Join", False); return
|
||||
# Info-Hint (T-Join unterstuetzt 1-Wand-Modus, L-Join braucht 2)
|
||||
n_wand_axes = sum(1 for o in sel
|
||||
if (o.Attributes.GetUserString("dossier_element_type")
|
||||
or "") == "wand_axis")
|
||||
if n_wand_axes == 0 and any(
|
||||
(o.Attributes.GetUserString("dossier_element_type") or "")
|
||||
.startswith("wand_") for o in sel):
|
||||
print("[SMART-JOIN] keine Wand-Achse in Selection — selektiere die "
|
||||
"Wand-Linie oder das Wand-Volumen.")
|
||||
|
||||
# T-Join: Endpunkt der einen Curve trifft mitten auf die andere → snap.
|
||||
# L-Join: beide Endpunkte werden zum Schnittpunkt der verlaengerten Linien
|
||||
# gezogen. T zuerst probieren (= spezifischer), dann L als Fallback.
|
||||
if len(sel) >= 2:
|
||||
# Diagnostic: was sieht smart_join in der Selection?
|
||||
axes_dbg, generic_dbg = _walls_and_curves_from_sel(doc, sel)
|
||||
type_counts = {}
|
||||
for o in sel:
|
||||
try:
|
||||
t = o.Attributes.GetUserString("dossier_element_type") or "<none>"
|
||||
wid_raw = o.Attributes.GetUserString("dossier_element_id") or ""
|
||||
geom_kind = type(o.Geometry).__name__
|
||||
key = "{}|{}|wid={}".format(t, geom_kind,
|
||||
"yes" if wid_raw else "no")
|
||||
type_counts[key] = type_counts.get(key, 0) + 1
|
||||
except Exception: pass
|
||||
print("[SMART-JOIN] sel-detect: {} Wand-Achsen, {} generische Curves "
|
||||
"(sel total: {})".format(len(axes_dbg), len(generic_dbg), len(sel)))
|
||||
for k, n in type_counts.items():
|
||||
print("[SMART-JOIN] {} × {}".format(n, k))
|
||||
try:
|
||||
if _t_join_attempt(doc, sel):
|
||||
doc.Views.Redraw()
|
||||
print("[SMART-JOIN] T-Join: Endpunkt auf Achse gesnappt")
|
||||
return
|
||||
else:
|
||||
print("[SMART-JOIN] T-Join: kein passender Kandidat (zu weit "
|
||||
"weg oder am Endpunkt → L-Join Territory)")
|
||||
except Exception as ex:
|
||||
print("[SMART-JOIN] T-Join error:", ex)
|
||||
try:
|
||||
if _l_join_attempt(doc, sel):
|
||||
doc.Views.Redraw()
|
||||
print("[SMART-JOIN] L-Join: 2 Curves zu L verbunden")
|
||||
return
|
||||
else:
|
||||
print("[SMART-JOIN] L-Join: konnte nicht ausfuehren (parallel, "
|
||||
"schon verbunden, oder Geometrie ungueltig)")
|
||||
except Exception as ex:
|
||||
print("[SMART-JOIN] L-Join error:", ex)
|
||||
|
||||
# Safety: wenn Wand-Achsen selektiert sind, NIE auf Standard-_Join fallen
|
||||
# — das wuerde mehrere Achsen zu einer Curve zusammenkleben und die Wand-
|
||||
# Verknuepfung zerstoeren (Source-Duplikat-Listener kapert die alte ID).
|
||||
has_wand_axis = any(
|
||||
obj.Attributes.GetUserString("dossier_element_type") == "wand_axis"
|
||||
for obj in sel)
|
||||
if has_wand_axis:
|
||||
print("[SMART-JOIN] Wand-Achsen selektiert: T-Join/L-Join hat nicht "
|
||||
"gegriffen (zu viele Selektionen oder zu weit weg). Bitte "
|
||||
"GENAU 2 Waende selektieren die sich verbinden sollen, dann "
|
||||
"erneut Cmd+J.")
|
||||
return
|
||||
|
||||
# Curves nach Closed/Open trennen
|
||||
closed_objs = []
|
||||
has_non_closed = False
|
||||
for obj in sel:
|
||||
g = obj.Geometry
|
||||
if isinstance(g, rg.Curve) and g.IsClosed:
|
||||
closed_objs.append(obj)
|
||||
else:
|
||||
has_non_closed = True
|
||||
|
||||
# Wenn nicht ALLE closed sind → einfach Standard-Join
|
||||
if has_non_closed or len(closed_objs) < 2:
|
||||
Rhino.RhinoApp.RunScript("_Join", False); return
|
||||
|
||||
# Gruppieren nach (Layer + Attrs + Fill)
|
||||
groups = {} # key → [obj, obj, ...]
|
||||
for obj in closed_objs:
|
||||
try:
|
||||
k = _attr_key(obj)
|
||||
except Exception:
|
||||
k = ("ungroup", id(obj))
|
||||
groups.setdefault(k, []).append(obj)
|
||||
|
||||
# gestaltung fuer Fill-Re-Apply
|
||||
_g = None
|
||||
try:
|
||||
import styles as _gmod; _g = _gmod
|
||||
except Exception as iex:
|
||||
print("[SMART-JOIN] gestaltung import:", iex)
|
||||
|
||||
tol = doc.ModelAbsoluteTolerance
|
||||
ur = doc.BeginUndoRecord("DOSSIER Smart-Join (gruppiert)")
|
||||
n_merged_total = 0
|
||||
n_groups_ops = 0
|
||||
try:
|
||||
for key, objs in groups.items():
|
||||
if len(objs) < 2: continue # einzelne Curve → nichts zu mergen
|
||||
try:
|
||||
curves = [o.Geometry for o in objs]
|
||||
result = rg.Curve.CreateBooleanUnion(curves, tol)
|
||||
except Exception as ex:
|
||||
print("[SMART-JOIN] BooleanUnion in Gruppe failed:", ex)
|
||||
continue
|
||||
if not result: continue
|
||||
# C) Pre-Check Overlap: wenn result-Anzahl gleich input-Anzahl
|
||||
# ist, gab's keinen tatsaechlichen Overlap → Gruppe nicht
|
||||
# anfassen.
|
||||
if len(result) >= len(objs):
|
||||
continue
|
||||
# Tatsaechlich gemerged → replace
|
||||
attrs_template = objs[0].Attributes.Duplicate()
|
||||
# Fill-Key clearen damit _apply_ebene_fill nicht "schon gefuellt"
|
||||
# zurueckgibt
|
||||
try:
|
||||
attrs_template.SetUserString("ebenen_fill_hatch_id", "")
|
||||
except Exception: pass
|
||||
|
||||
any_had_fill = bool(key[4][0]) # fill_key[0] = had-fill bool
|
||||
|
||||
new_ids = []
|
||||
for crv in result:
|
||||
nid = doc.Objects.AddCurve(crv, attrs_template)
|
||||
if nid: new_ids.append(nid)
|
||||
for o in objs:
|
||||
try: doc.Objects.Delete(o.Id, True)
|
||||
except Exception: pass
|
||||
# Fill nachziehen wenn Inputs welche hatten
|
||||
if any_had_fill and _g is not None:
|
||||
for nid in new_ids:
|
||||
try:
|
||||
nobj = doc.Objects.FindId(nid)
|
||||
if nobj is not None:
|
||||
_g._apply_ebene_fill(doc, nobj)
|
||||
except Exception as fex:
|
||||
print("[SMART-JOIN] fill-apply:", fex)
|
||||
n_merged_total += (len(objs) - len(result))
|
||||
n_groups_ops += 1
|
||||
finally:
|
||||
doc.EndUndoRecord(ur)
|
||||
|
||||
if n_groups_ops == 0:
|
||||
print("[SMART-JOIN] Nichts zu mergen — keine Curves overlappen "
|
||||
"(oder verschiedene Attribute/Layer)")
|
||||
else:
|
||||
doc.Views.Redraw()
|
||||
print("[SMART-JOIN] {} Gruppe(n) bearbeitet, {} Curve(s) zu Union vereint"
|
||||
.format(n_groups_ops, n_merged_total))
|
||||
|
||||
|
||||
_run()
|
||||
@@ -0,0 +1,267 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Smart-Split: User zeichnet eine Splitlinie/Polylinie waehrend des Befehls
|
||||
# (mehrere Klicks, Enter beendet die Eingabe). Alle Curves die die Linie
|
||||
# schneidet werden gesplittet.
|
||||
# - Offene Curves: bei den Schnittpunkten in offene Segmente.
|
||||
# - GESCHLOSSENE Curves: in mehrere CLOSED Sub-Regionen via
|
||||
# Curve.CreateBooleanRegions (funktioniert auch bei multi-segment
|
||||
# Polylinien-Cuttern). Per-Object-Hatch wird auf alle Regionen repliziert.
|
||||
# DOSSIER-Source-Typen (Wand-Achse etc.) bleiben geschuetzt.
|
||||
import scriptcontext as sc
|
||||
import Rhino
|
||||
import Rhino.Input.Custom as ric
|
||||
import Rhino.Geometry as rg
|
||||
import Rhino.DocObjects as rdoc
|
||||
from Rhino.Input import GetResult
|
||||
|
||||
|
||||
# Was Smart-Split NIE anfasst:
|
||||
# - oeffnung_point / stuetze_point: Punkte, nicht teilbar
|
||||
# - schnitt_axis: Schnitt-Linien sollen bleiben, sonst kaputte Schnitte
|
||||
# - treppe_axis: Treppen-State (Lauflinie, Schrittmass-Lock, Wendel-Sweep)
|
||||
# waere bei einem Split inkonsistent
|
||||
# Alles andere (wand/traeger/decke/dach/raum/aussparung) DARF gesplittet werden:
|
||||
# der Add-Listener in elemente.py erkennt die Duplikat-IDs der neuen Stuecke
|
||||
# und vergibt jedem Stueck ein frisches Element-ID + Regen → BIM-Volumen
|
||||
# baut sich pro neuem Stueck neu auf.
|
||||
_PROTECTED_TYPES = {
|
||||
"treppe_axis",
|
||||
"oeffnung_point", "stuetze_point", "schnitt_axis",
|
||||
}
|
||||
|
||||
|
||||
def _capture_hatch_props(doc, src_obj):
|
||||
try:
|
||||
sa = src_obj.Attributes
|
||||
fill_hid = sa.GetUserString("ebenen_fill_hatch_id") or ""
|
||||
if not fill_hid: return None
|
||||
import System
|
||||
hid = System.Guid(fill_hid)
|
||||
hobj = doc.Objects.FindId(hid)
|
||||
if hobj is None or hobj.IsDeleted: return None
|
||||
hg = hobj.Geometry
|
||||
ha = hobj.Attributes
|
||||
if not hasattr(hg, "PatternIndex"): return None
|
||||
return {
|
||||
"pattern_idx": int(hg.PatternIndex),
|
||||
"scale": float(hg.PatternScale),
|
||||
"rotation": float(hg.PatternRotation),
|
||||
"layer_idx": int(ha.LayerIndex),
|
||||
"color_source": int(ha.ColorSource),
|
||||
"color_argb": int(ha.ObjectColor.ToArgb()),
|
||||
"plot_color_source": int(ha.PlotColorSource),
|
||||
"plot_color_argb": int(ha.PlotColor.ToArgb()),
|
||||
"linetype_source": int(ha.LinetypeSource),
|
||||
"linetype_idx": int(ha.LinetypeIndex),
|
||||
"fill_source": sa.GetUserString("ebenen_fill_source") or "object",
|
||||
}
|
||||
except Exception as ex:
|
||||
print("[SMART-SPLIT] capture-hatch:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def _replicate_hatch(doc, new_obj, hp):
|
||||
if hp is None: return
|
||||
import System
|
||||
try:
|
||||
crv = new_obj.Geometry
|
||||
if not isinstance(crv, rg.Curve) or not crv.IsClosed: return
|
||||
tol = doc.ModelAbsoluteTolerance
|
||||
hatches = rg.Hatch.Create(crv, hp["pattern_idx"], hp["rotation"],
|
||||
hp["scale"], tol)
|
||||
if not hatches or len(hatches) == 0: return
|
||||
ha = rdoc.ObjectAttributes()
|
||||
ha.LayerIndex = hp["layer_idx"]
|
||||
ha.ColorSource = rdoc.ObjectColorSource(hp["color_source"])
|
||||
ha.ObjectColor = System.Drawing.Color.FromArgb(hp["color_argb"])
|
||||
try:
|
||||
ha.PlotColorSource = rdoc.ObjectPlotColorSource(hp["plot_color_source"])
|
||||
ha.PlotColor = System.Drawing.Color.FromArgb(hp["plot_color_argb"])
|
||||
except Exception: pass
|
||||
if hp["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
|
||||
ha.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
|
||||
ha.LinetypeIndex = hp["linetype_idx"]
|
||||
ha.SetUserString("ebenen_fill_source", hp.get("fill_source", "object"))
|
||||
ha.SetUserString("ebenen_fill_owner", str(new_obj.Id))
|
||||
new_hid = doc.Objects.AddHatch(hatches[0], ha)
|
||||
if new_hid and new_hid != System.Guid.Empty:
|
||||
ca = new_obj.Attributes.Duplicate()
|
||||
ca.SetUserString("ebenen_fill_hatch_id", str(new_hid))
|
||||
ca.SetUserString("ebenen_fill_source", hp.get("fill_source", "object"))
|
||||
doc.Objects.ModifyAttributes(new_obj, ca, True)
|
||||
except Exception as ex:
|
||||
print("[SMART-SPLIT] hatch-replicate:", ex)
|
||||
|
||||
|
||||
def _collect_polyline_cutter(prompt_first, prompt_more):
|
||||
"""Sammelt n Punkte fuer den Cutter. Enter beendet (min. 2 Punkte).
|
||||
ESC bricht ab. Returnt Polyline oder None."""
|
||||
pts = []
|
||||
while True:
|
||||
gp = ric.GetPoint()
|
||||
if not pts:
|
||||
gp.SetCommandPrompt(prompt_first)
|
||||
else:
|
||||
gp.SetCommandPrompt(prompt_more + " (Enter zum Splitten, ESC = abbrechen)")
|
||||
gp.SetBasePoint(pts[-1], True)
|
||||
gp.DrawLineFromPoint(pts[-1], True)
|
||||
gp.AcceptNothing(True)
|
||||
res = gp.Get()
|
||||
if res == GetResult.Nothing:
|
||||
# Enter gedrueckt
|
||||
if len(pts) >= 2: return rg.Polyline(pts)
|
||||
print("[SMART-SPLIT] Mindestens 2 Punkte noetig"); return None
|
||||
if res != GetResult.Point: return None
|
||||
pts.append(gp.Point())
|
||||
|
||||
|
||||
def _split_closed_with_cutter(closed_crv, cutter_crv, doc):
|
||||
"""Splittet closed curve mit beliebigem cutter (Linie oder Polylinie) in
|
||||
closed Sub-Regionen via Curve.CreateBooleanRegions."""
|
||||
tol = doc.ModelAbsoluteTolerance
|
||||
try:
|
||||
# WorldXY-Plane als Default (DOSSIER ist 2D Plan-Workflow)
|
||||
plane = rg.Plane.WorldXY
|
||||
regions = rg.Curve.CreateBooleanRegions(
|
||||
[closed_crv, cutter_crv], plane, False, tol)
|
||||
if regions is None or regions.RegionCount == 0:
|
||||
return None
|
||||
out = []
|
||||
for i in range(regions.RegionCount):
|
||||
rcurves = list(regions.RegionCurves(i))
|
||||
if not rcurves: continue
|
||||
if len(rcurves) == 1:
|
||||
if rcurves[0].IsClosed:
|
||||
out.append(rcurves[0])
|
||||
else:
|
||||
# einzelne offene curve — sollte nicht passieren bei
|
||||
# Boolean-Regions, aber defensiv
|
||||
joined = rg.Curve.JoinCurves([rcurves[0]], tol)
|
||||
if joined and len(joined) > 0 and joined[0].IsClosed:
|
||||
out.append(joined[0])
|
||||
else:
|
||||
joined = rg.Curve.JoinCurves(rcurves, tol)
|
||||
if joined:
|
||||
for j in joined:
|
||||
if j.IsClosed: out.append(j)
|
||||
return out if out else None
|
||||
except Exception as ex:
|
||||
print("[SMART-SPLIT] closed-split:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def _run():
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
|
||||
# Polylinie als Cutter sammeln
|
||||
poly = _collect_polyline_cutter(
|
||||
"Splitlinie Startpunkt",
|
||||
"Naechster Punkt")
|
||||
if poly is None or poly.Count < 2:
|
||||
return
|
||||
cutter = rg.PolylineCurve(poly)
|
||||
tol = doc.ModelAbsoluteTolerance
|
||||
|
||||
pre_sel = [o for o in doc.Objects.GetSelectedObjects(False, False)
|
||||
if o is not None and not o.IsDeleted]
|
||||
if pre_sel:
|
||||
source = pre_sel
|
||||
mode_label = "selektierte ({})".format(len(pre_sel))
|
||||
else:
|
||||
s = rdoc.ObjectEnumeratorSettings()
|
||||
s.HiddenObjects = False; s.LockedObjects = False
|
||||
source = list(doc.Objects.GetObjectList(s))
|
||||
mode_label = "alle sichtbaren"
|
||||
|
||||
candidates_open = []
|
||||
candidates_closed = []
|
||||
for obj in source:
|
||||
if obj is None or obj.IsDeleted: continue
|
||||
try:
|
||||
t = obj.Attributes.GetUserString("dossier_element_type") or ""
|
||||
if t in _PROTECTED_TYPES: continue
|
||||
except Exception: pass
|
||||
g = obj.Geometry
|
||||
if not isinstance(g, rg.Curve): continue
|
||||
try:
|
||||
ints = rg.Intersect.Intersection.CurveCurve(cutter, g, tol, tol)
|
||||
except Exception:
|
||||
continue
|
||||
if not ints or ints.Count == 0: continue
|
||||
|
||||
if g.IsClosed:
|
||||
candidates_closed.append((obj, g))
|
||||
else:
|
||||
params = []
|
||||
for i in range(ints.Count):
|
||||
ev = ints[i]
|
||||
if ev.IsPoint:
|
||||
params.append(ev.ParameterB)
|
||||
else:
|
||||
params.append(ev.ParameterB); params.append(ev.ParameterB2)
|
||||
if params:
|
||||
params = sorted(set(round(p, 6) for p in params))
|
||||
candidates_open.append((obj, g, params))
|
||||
|
||||
if not candidates_open and not candidates_closed:
|
||||
print("[SMART-SPLIT] Cutter schneidet nichts ({})".format(mode_label))
|
||||
return
|
||||
|
||||
ur = doc.BeginUndoRecord("DOSSIER Smart-Split")
|
||||
n_open = 0; n_closed = 0
|
||||
try:
|
||||
# Closed: Boolean-Regions → CLOSED Sub-Regionen + Fill replicate
|
||||
for obj, crv in candidates_closed:
|
||||
try:
|
||||
regions = _split_closed_with_cutter(crv, cutter, doc)
|
||||
if not regions or len(regions) <= 1: continue
|
||||
hatch_props = _capture_hatch_props(doc, obj)
|
||||
attrs = obj.Attributes.Duplicate()
|
||||
try: attrs.SetUserString("ebenen_fill_hatch_id", "")
|
||||
except Exception: pass
|
||||
new_ids = []
|
||||
for r in regions:
|
||||
nid = doc.Objects.AddCurve(r, attrs)
|
||||
if nid: new_ids.append(nid)
|
||||
doc.Objects.Delete(obj.Id, True)
|
||||
if hatch_props is not None:
|
||||
for nid in new_ids:
|
||||
nobj = doc.Objects.FindId(nid)
|
||||
if nobj is not None:
|
||||
_replicate_hatch(doc, nobj, hatch_props)
|
||||
else:
|
||||
try:
|
||||
import styles as _gmod
|
||||
for nid in new_ids:
|
||||
nobj = doc.Objects.FindId(nid)
|
||||
if nobj is not None:
|
||||
_gmod._apply_ebene_fill(doc, nobj)
|
||||
except Exception: pass
|
||||
n_closed += 1
|
||||
except Exception as ex:
|
||||
print("[SMART-SPLIT] closed-fail:", ex)
|
||||
|
||||
# Open: split bei Params
|
||||
for obj, crv, params in candidates_open:
|
||||
try:
|
||||
pieces = crv.Split(params)
|
||||
if not pieces or len(pieces) <= 1: continue
|
||||
attrs = obj.Attributes.Duplicate()
|
||||
for p in pieces:
|
||||
doc.Objects.AddCurve(p, attrs)
|
||||
doc.Objects.Delete(obj.Id, True)
|
||||
n_open += 1
|
||||
except Exception as ex:
|
||||
print("[SMART-SPLIT] open-fail:", ex)
|
||||
finally:
|
||||
doc.EndUndoRecord(ur)
|
||||
|
||||
doc.Views.Redraw()
|
||||
print("[SMART-SPLIT] {} closed-Regionen + {} offene Curves gesplittet "
|
||||
"({} Cutter-Punkte, {})"
|
||||
.format(n_closed, n_open, poly.Count, mode_label))
|
||||
|
||||
|
||||
_run()
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'stempel'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("stempel")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'stuetze'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("stuetze")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'symbol'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("symbol")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'traeger'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("traeger")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'treppe'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("treppe")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'tuer'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("tuer")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer Alias 'wand'. Importiert dossier_dispatch + ruft Action.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_dispatch
|
||||
dossier_dispatch.dispatch("wand")
|
||||
@@ -0,0 +1,97 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
dossier_dispatch.py
|
||||
Universal-Wrapper fuer DOSSIER-Bridge-Commands via Rhino-Alias.
|
||||
|
||||
Aufruf vom Alias:
|
||||
_-RunPythonScript "/.../dossier_dispatch.py" <action>
|
||||
oder via Rhino.Input.RhinoGet — wir lesen den letzten String-Parameter
|
||||
aus der Command-Line.
|
||||
|
||||
Aktionen mappen auf ElementeBridge._cmd_create_* via einer kleinen
|
||||
Dispatch-Tabelle. Bridge-Referenz wird in sc.sticky vom panel_factory
|
||||
abgelegt (siehe elemente.py _bridge_factory).
|
||||
"""
|
||||
import sys
|
||||
import scriptcontext as sc
|
||||
|
||||
|
||||
_ACTIONS = {
|
||||
"wand": ("_cmd_create_wall", ()),
|
||||
"tuer": ("_cmd_create_oeffnung", ("tuer",)),
|
||||
"fenster": ("_cmd_create_oeffnung", ("fenster",)),
|
||||
"decke": ("_cmd_create_decke", ()),
|
||||
"aussparung":("_cmd_create_aussparung",()),
|
||||
"dach": ("_cmd_create_dach", ()),
|
||||
"treppe": ("_cmd_create_treppe", ()),
|
||||
"stuetze": ("_cmd_create_stuetze", ()),
|
||||
"traeger": ("_cmd_create_traeger", ()),
|
||||
"raum": ("_cmd_create_raum", ()),
|
||||
"stempel": ("_cmd_create_stempel", ()),
|
||||
"symbol": ("_cmd_create_symbol", ()),
|
||||
}
|
||||
|
||||
|
||||
_PRETTY = {
|
||||
"wand": "DOSSIER Wand",
|
||||
"tuer": "DOSSIER Tuer",
|
||||
"fenster": "DOSSIER Fenster",
|
||||
"decke": "DOSSIER Decke",
|
||||
"aussparung": "DOSSIER Aussparung",
|
||||
"dach": "DOSSIER Dach",
|
||||
"treppe": "DOSSIER Treppe",
|
||||
"stuetze": "DOSSIER Stuetze",
|
||||
"traeger": "DOSSIER Traeger",
|
||||
"raum": "DOSSIER Raum",
|
||||
"stempel": "DOSSIER Stempel",
|
||||
"symbol": "DOSSIER Symbol",
|
||||
}
|
||||
|
||||
|
||||
def dispatch(action):
|
||||
"""Public entry — von per-action Wrapper-Scripts aufgerufen."""
|
||||
try:
|
||||
import Rhino
|
||||
Rhino.RhinoApp.SetCommandPrompt(_PRETTY.get(action, "DOSSIER " + action.capitalize()))
|
||||
except Exception: pass
|
||||
bridge = sc.sticky.get("dossier_bridge_elemente")
|
||||
if bridge is None:
|
||||
print("[DOSSIER-ALIAS] Elemente-Bridge nicht aktiv (Panel oeffnen)")
|
||||
return
|
||||
spec = _ACTIONS.get(action)
|
||||
if spec is None:
|
||||
print("[DOSSIER-ALIAS] Unbekannte Aktion:", action)
|
||||
return
|
||||
method_name, args = spec
|
||||
method = getattr(bridge, method_name, None)
|
||||
if method is None:
|
||||
print("[DOSSIER-ALIAS] Bridge-Method fehlt:", method_name)
|
||||
return
|
||||
try:
|
||||
method({}, *args)
|
||||
except Exception as ex:
|
||||
print("[DOSSIER-ALIAS]", action, "->", method_name, ":", ex)
|
||||
|
||||
|
||||
# Backwards-Compat (alter Name).
|
||||
_dispatch = dispatch
|
||||
|
||||
|
||||
def _read_action_from_argv():
|
||||
# sys.argv enthaelt bei _-RunPythonScript "path" arg1 arg2 ... die
|
||||
# Args nach dem Skript-Pfad. argv[0] = Skript-Pfad.
|
||||
if len(sys.argv) >= 2:
|
||||
return str(sys.argv[1]).strip().lower()
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = _read_action_from_argv()
|
||||
if a:
|
||||
_dispatch(a)
|
||||
else:
|
||||
print("[DOSSIER-ALIAS] Keine Aktion uebergeben. Erwartet:",
|
||||
", ".join(sorted(_ACTIONS.keys())))
|
||||
@@ -0,0 +1,72 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
dossier_view_mode.py
|
||||
Setzt Display-Mode (+ optional Standard-Ansicht) im aktiven Viewport.
|
||||
|
||||
Aufruf:
|
||||
_-RunPythonScript "/.../dossier_view_mode.py" <mode>
|
||||
mode: plan | persp3d | material | raytracing
|
||||
"""
|
||||
import sys
|
||||
import Rhino
|
||||
|
||||
|
||||
_MODES = {
|
||||
"plan": {"display": "Dossier Plan", "view": "Top", "label": "DOSSIER Plan-Mode"},
|
||||
"persp3d": {"display": "Dossier 3D", "view": "Perspective","label": "DOSSIER 3D-Mode"},
|
||||
"material": {"display": "Dossier Material", "view": None, "label": "DOSSIER Material-Mode"},
|
||||
"raytracing": {"display": "Dossier Raytracing", "view": None, "label": "DOSSIER Raytracing"},
|
||||
}
|
||||
|
||||
|
||||
def _apply(mode_name):
|
||||
spec = _MODES.get(mode_name)
|
||||
if spec is None:
|
||||
print("[VIEW-MODE] Unbekannt:", mode_name)
|
||||
return
|
||||
try: Rhino.RhinoApp.SetCommandPrompt(spec.get("label", "DOSSIER View"))
|
||||
except Exception: pass
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None:
|
||||
print("[VIEW-MODE] Kein aktives Doc")
|
||||
return
|
||||
view = doc.Views.ActiveView
|
||||
if view is None:
|
||||
print("[VIEW-MODE] Kein aktiver Viewport")
|
||||
return
|
||||
# Standard-View setzen (Top / Perspective) falls definiert
|
||||
vw_name = spec["view"]
|
||||
if vw_name:
|
||||
try:
|
||||
view.ActiveViewport.SetProjection(
|
||||
Rhino.Display.DefinedViewportProjection.Top
|
||||
if vw_name == "Top"
|
||||
else Rhino.Display.DefinedViewportProjection.Perspective,
|
||||
vw_name, True)
|
||||
except Exception as ex:
|
||||
print("[VIEW-MODE] view-set:", ex)
|
||||
# Display-Mode setzen via Description-Lookup
|
||||
dm_name = spec["display"]
|
||||
try:
|
||||
all_dm = Rhino.Display.DisplayModeDescription.GetDisplayModes()
|
||||
target = None
|
||||
for d in all_dm:
|
||||
if d.EnglishName == dm_name or d.LocalName == dm_name:
|
||||
target = d; break
|
||||
if target is None:
|
||||
print("[VIEW-MODE] Display-Mode not found:", dm_name)
|
||||
return
|
||||
view.ActiveViewport.DisplayMode = target
|
||||
view.Redraw()
|
||||
except Exception as ex:
|
||||
print("[VIEW-MODE] display-mode:", ex)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) >= 2:
|
||||
_apply(str(sys.argv[1]).strip().lower())
|
||||
else:
|
||||
print("[VIEW-MODE] Erwartet Mode-Name:", ", ".join(_MODES.keys()))
|
||||
@@ -0,0 +1,469 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
aliases/loader.py
|
||||
Liest shortcuts_default.json + User-Overrides aus dossier_settings.json,
|
||||
merged und wendet via Rhino.ApplicationSettings.CommandAliasList /
|
||||
ShortcutKeySettings an. Wird einmal beim Rhino-Start aus startup.py
|
||||
aufgerufen (idempotent — SetMacro ueberschreibt).
|
||||
|
||||
User-Override-Format in dossier_settings.json:
|
||||
"shortcuts_user": {
|
||||
"<action_id>": "<trigger_string>" // leer = Default
|
||||
}
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import Rhino
|
||||
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_quit_xml_pairs = [] # gefuellt in apply_all(), genutzt vom Closing-Hook
|
||||
_DEFAULTS_PATH = os.path.join(_HERE, "shortcuts_default.json")
|
||||
_SETTINGS_PATHS = [
|
||||
os.path.expanduser("~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json"),
|
||||
os.path.expanduser("~/Library/Application Support/RhinoPanel/dossier_settings.json"), # legacy
|
||||
]
|
||||
|
||||
|
||||
def _read_defaults():
|
||||
try:
|
||||
with open(_DEFAULTS_PATH, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
out = {}
|
||||
for k, v in data.items():
|
||||
if k.startswith("_"): continue
|
||||
if not isinstance(v, dict): continue
|
||||
out[k] = v
|
||||
return out
|
||||
except Exception as ex:
|
||||
print("[ALIASES] Defaults lesen:", ex)
|
||||
return {}
|
||||
|
||||
|
||||
def _read_user_overrides():
|
||||
"""Liest 'shortcuts_user' aus dossier_settings.json. Format:
|
||||
{ action_id: trigger_string }. Leerer String / None = Default."""
|
||||
for path in _SETTINGS_PATHS:
|
||||
if not os.path.exists(path): continue
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
so = data.get("shortcuts_user")
|
||||
if isinstance(so, dict): return so
|
||||
except Exception as ex:
|
||||
print("[ALIASES] Settings lesen:", ex)
|
||||
return {}
|
||||
|
||||
|
||||
def _expand_macro(macro):
|
||||
"""Platzhalter {ALIASDIR} → absoluter Pfad zum aliases/-Ordner."""
|
||||
return macro.replace("{ALIASDIR}", _HERE)
|
||||
|
||||
|
||||
# Sonderzeichen → Rhino-Enum-Namen (Mac XML + ShortcutKey-API)
|
||||
_SPECIAL_KEY_NAMES = {
|
||||
"-": "Minus", "+": "Plus", "=": "Equals",
|
||||
"/": "Slash", "\\": "Backslash",
|
||||
".": "Period", ",": "Comma",
|
||||
";": "Semicolon", "'": "Quote", "`": "Backquote",
|
||||
"[": "OpenBracket", "]": "CloseBracket",
|
||||
}
|
||||
|
||||
|
||||
def _normalize_key_part(key_part):
|
||||
"""Mapped Sonderzeichen wie '-' auf Enum-Namen ('Minus'). Buchstaben/F-Keys
|
||||
bleiben unchanged (Case-preserved)."""
|
||||
if key_part in _SPECIAL_KEY_NAMES:
|
||||
return _SPECIAL_KEY_NAMES[key_part]
|
||||
return key_part
|
||||
|
||||
|
||||
def _xml_key_from_trigger(trigger):
|
||||
"""'Cmd+Shift+F3' → 'CommandShiftF3' (Mac Rhino XML-Schema).
|
||||
Cmd/Ctrl → 'Command', Shift → 'Shift', Alt/Option → 'Option'.
|
||||
Sonderzeichen ('-', '/', etc.) werden auf Enum-Namen gemapped."""
|
||||
t = trigger.replace(" ", "")
|
||||
parts = t.split("+") if "+" in t[1:] else [t]
|
||||
# Edge-Case: trigger endet auf literal '+' oder '-' → letztes Element ist Key
|
||||
# 'Cmd+-' → ['Cmd', '', '-'] via split. Fix: re-split last token wenn leer
|
||||
parts = [p for p in parts if p != ""]
|
||||
# Sonderfall trigger == 'Cmd+-' → split('+') = ['Cmd', '-'], OK
|
||||
# Sonderfall trigger == 'Cmd++' → split('+') = ['Cmd', '', ''] → key = '+'
|
||||
if "Cmd++" in trigger or "Ctrl++" in trigger or "Shift++" in trigger:
|
||||
parts = trigger.replace(" ", "").rstrip("+").split("+") + ["+"]
|
||||
if not parts: return None
|
||||
key_part = _normalize_key_part(parts[-1])
|
||||
mods = set(p.lower() for p in parts[:-1])
|
||||
has_cmd = ("cmd" in mods) or ("ctrl" in mods) or ("command" in mods)
|
||||
has_shift = "shift" in mods
|
||||
has_alt = ("alt" in mods) or ("option" in mods) or ("opt" in mods)
|
||||
prefix = ""
|
||||
if has_cmd: prefix += "Command"
|
||||
if has_shift: prefix += "Shift"
|
||||
if has_alt: prefix += "Option"
|
||||
return prefix + key_part
|
||||
|
||||
|
||||
def _entry_in_xml(xml_key, expected_macro):
|
||||
"""True wenn <entry key='<xml_key>'>expected_macro</entry> bereits im
|
||||
Mac Rhino settings-XML existiert."""
|
||||
import os
|
||||
import re
|
||||
paths = [
|
||||
os.path.expanduser("~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"),
|
||||
]
|
||||
_esc = lambda s: s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
pat = re.compile(
|
||||
r'<entry\s+key="' + re.escape(xml_key) + r'"\s*>([^<]*)</entry>')
|
||||
for path in paths:
|
||||
if not os.path.exists(path): continue
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
m = pat.search(content)
|
||||
if m and m.group(1) == _esc(expected_macro):
|
||||
return True
|
||||
except Exception: pass
|
||||
return False
|
||||
|
||||
|
||||
def _xml_persist_shortcut(xml_key, macro, verbose=False):
|
||||
"""Schreibt <entry key="<xml_key>"><macro></entry> direkt in Mac Rhino's
|
||||
settings-Scheme__Default.xml unter <child key='ShortcutKeys'>. String-
|
||||
basiert damit die Original-Formatierung 1:1 erhalten bleibt."""
|
||||
import os
|
||||
import re
|
||||
paths = [
|
||||
os.path.expanduser("~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"),
|
||||
]
|
||||
n_written = 0
|
||||
_esc = lambda s: s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
for path in paths:
|
||||
if not os.path.exists(path): continue
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
new_entry = '<entry key="{}">{}</entry>'.format(xml_key, _esc(macro))
|
||||
|
||||
# Existing entry? Loeschen (mit umgebendem Whitespace+Newline)
|
||||
# und neu hinzufuegen mit sauberem Format. Vermeidet
|
||||
# kaputt-formatierte Entries.
|
||||
pat = re.compile(
|
||||
r'<entry\s+key="' + re.escape(xml_key) + r'"\s*(/>|>[^<]*</entry>)')
|
||||
m = pat.search(content)
|
||||
if m:
|
||||
# Check Line-Kontext: nur diese Entry auf Zeile + unchanged?
|
||||
line_start = content.rfind("\n", 0, m.start()) + 1
|
||||
line_end = content.find("\n", m.end())
|
||||
if line_end < 0: line_end = len(content)
|
||||
line_trim = content[line_start:line_end].strip()
|
||||
if line_trim == new_entry:
|
||||
if verbose: print("[ALIASES] XML '{}' unchanged".format(xml_key))
|
||||
continue
|
||||
# Sonst: loeschen inkl. preceding-newline+whitespace damit
|
||||
# keine orphan-line uebrig bleibt
|
||||
del_start = m.start()
|
||||
while del_start > 0 and content[del_start-1] in " \t":
|
||||
del_start -= 1
|
||||
if del_start > 0 and content[del_start-1] == "\n":
|
||||
del_start -= 1
|
||||
content = content[:del_start] + content[m.end():]
|
||||
if True:
|
||||
# ShortcutKeys-Section finden
|
||||
sec_start = content.find('<child key="ShortcutKeys">')
|
||||
if sec_start < 0:
|
||||
if verbose: print("[ALIASES] ShortcutKeys-section fehlt")
|
||||
continue
|
||||
sec_end = content.find('</child>', sec_start)
|
||||
if sec_end < 0:
|
||||
if verbose: print("[ALIASES] ShortcutKeys-close fehlt")
|
||||
continue
|
||||
# Indent vom letzten <entry> in der Section uebernehmen
|
||||
section = content[sec_start:sec_end]
|
||||
ms = list(re.finditer(r'\n([ \t]*)<entry\s', section))
|
||||
entry_indent = ms[-1].group(1) if ms else " "
|
||||
# Indent vor </child> (typisch 6 spaces)
|
||||
close_match = re.search(r'\n([ \t]*)$', content[:sec_end])
|
||||
close_indent = close_match.group(1) if close_match else " "
|
||||
# Section neu zusammensetzen: alles vor </child> bereinigt
|
||||
# + sauberer Insert
|
||||
before = content[:sec_end].rstrip(" \t") + "\n"
|
||||
content = (before + entry_indent + new_entry + "\n"
|
||||
+ close_indent + content[sec_end:])
|
||||
action = "added"
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
n_written += 1
|
||||
if verbose: print("[ALIASES] XML {} '{}'".format(action, xml_key))
|
||||
except Exception as ex:
|
||||
print("[ALIASES] XML-Write {}: {}".format(path, ex))
|
||||
return n_written
|
||||
|
||||
|
||||
def _install_quit_xml_save(pairs):
|
||||
"""Rhino's Closing-Event fired auf Mac NICHT zuverlaessig. Wir
|
||||
installieren MEHRERE Hooks parallel:
|
||||
1. Rhino.RhinoApp.Closing (Mac: meist No-op, Windows: ok)
|
||||
2. Python atexit (laeuft wenn Interpreter terminiert)
|
||||
3. AppDomain.ProcessExit (.NET-Level Hook)
|
||||
4. Idle-Watcher: schreibt XML alle 30s wenn Aenderung erkannt
|
||||
(Fallback fuer Rhino's runtime-flush)
|
||||
Marker-Logging zur Verifikation welcher Hook wirklich feuert."""
|
||||
import os as _os
|
||||
import datetime as _dt
|
||||
_marker = _os.path.expanduser("~/Library/Logs/dossier_quit_hook.log")
|
||||
try:
|
||||
_os.makedirs(_os.path.dirname(_marker), exist_ok=True)
|
||||
except Exception: pass
|
||||
|
||||
def _log(msg):
|
||||
try:
|
||||
with open(_marker, "a") as f:
|
||||
f.write("[{}] {}\n".format(_dt.datetime.now().isoformat(), msg))
|
||||
except Exception: pass
|
||||
|
||||
def _write_all(source):
|
||||
n_ok = 0
|
||||
for xml_key, macro in pairs:
|
||||
if _xml_persist_shortcut(xml_key, macro, verbose=False) > 0:
|
||||
n_ok += 1
|
||||
_log("{} FIRED — {}/{} ok".format(source, n_ok, len(pairs)))
|
||||
return n_ok
|
||||
|
||||
n_hooks = 0
|
||||
try:
|
||||
import Rhino
|
||||
def _on_closing(*_):
|
||||
try: _write_all("RhinoClosing")
|
||||
except Exception as ex: _log("RhinoClosing ERROR: {}".format(ex))
|
||||
Rhino.RhinoApp.Closing += _on_closing
|
||||
n_hooks += 1
|
||||
except Exception as ex:
|
||||
_log("RhinoClosing install err: {}".format(ex))
|
||||
|
||||
try:
|
||||
import atexit
|
||||
def _on_atexit():
|
||||
try: _write_all("atexit")
|
||||
except Exception as ex: _log("atexit ERROR: {}".format(ex))
|
||||
atexit.register(_on_atexit)
|
||||
n_hooks += 1
|
||||
except Exception as ex:
|
||||
_log("atexit install err: {}".format(ex))
|
||||
|
||||
try:
|
||||
import System
|
||||
def _on_process_exit(*_):
|
||||
try: _write_all("ProcessExit")
|
||||
except Exception as ex: _log("ProcessExit ERROR: {}".format(ex))
|
||||
System.AppDomain.CurrentDomain.ProcessExit += _on_process_exit
|
||||
n_hooks += 1
|
||||
except Exception as ex:
|
||||
_log("ProcessExit install err: {}".format(ex))
|
||||
|
||||
# Idle-Watcher: periodisch (alle ~30s) checken ob unsere XML-Entries
|
||||
# noch da sind. Wenn nein → wieder reinschreiben. Ueberlebt Rhino-
|
||||
# Runtime-Flushes auch ohne Close-Event.
|
||||
try:
|
||||
import Rhino
|
||||
import time as _time
|
||||
_state = {"last": 0.0}
|
||||
def _idle_watcher(*_):
|
||||
try:
|
||||
now = _time.time()
|
||||
if now - _state["last"] < 30.0: return
|
||||
_state["last"] = now
|
||||
# Pruefen ob entries fehlen — wenn ja, alle re-schreiben
|
||||
_write_all("IdleWatch")
|
||||
except Exception as ex:
|
||||
_log("IdleWatch ERROR: {}".format(ex))
|
||||
Rhino.RhinoApp.Idle += _idle_watcher
|
||||
n_hooks += 1
|
||||
_log("IdleWatch installed (30s interval)")
|
||||
except Exception as ex:
|
||||
_log("IdleWatch install err: {}".format(ex))
|
||||
|
||||
_log("Hooks INSTALLED ({} of 4) for {} shortcuts".format(n_hooks, len(pairs)))
|
||||
# Initiale Schreibung im ersten Pass auch — falls Rhino sofort flusht
|
||||
_write_all("InitialWrite")
|
||||
return n_hooks > 0
|
||||
|
||||
|
||||
def _resolve_fkey(trigger):
|
||||
"""'F3' / 'Shift+F3' / 'Cmd+F3' / 'Cmd+Alt+F3' → ShortcutKey-Enum-Wert.
|
||||
Enum-Naming-Konvention von Rhino: Ctrl → Shift → Alt → KeyName
|
||||
(z.B. CtrlAltF3, CtrlShiftAltF3). Cmd auf Mac mappt auf Ctrl,
|
||||
Option/Opt auf Alt. Sonderzeichen via _SPECIAL_KEY_NAMES."""
|
||||
SK = Rhino.ApplicationSettings.ShortcutKey
|
||||
t = trigger.replace(" ", "")
|
||||
parts = t.split("+")
|
||||
parts = [p for p in parts if p != ""]
|
||||
if not parts: return None
|
||||
raw_last = parts[-1]
|
||||
if raw_last in _SPECIAL_KEY_NAMES:
|
||||
key_part = _SPECIAL_KEY_NAMES[raw_last]
|
||||
else:
|
||||
key_part = raw_last.upper()
|
||||
mods = set(p.lower() for p in parts[:-1])
|
||||
has_ctrl = ("ctrl" in mods) or ("cmd" in mods) or ("command" in mods)
|
||||
has_shift = "shift" in mods
|
||||
has_alt = ("alt" in mods) or ("option" in mods) or ("opt" in mods)
|
||||
prefix = ""
|
||||
if has_ctrl: prefix += "Ctrl"
|
||||
if has_shift: prefix += "Shift"
|
||||
if has_alt: prefix += "Alt"
|
||||
return getattr(SK, prefix + key_part, None)
|
||||
|
||||
|
||||
def _resolve_cmd_letter(trigger):
|
||||
"""'Cmd+W' / 'Cmd+Shift+W' → ShortcutKey-Enum (Ctrl* auf Rhino-Naming-
|
||||
Konvention; Mac mappt Ctrl auf Cmd intern)."""
|
||||
SK = Rhino.ApplicationSettings.ShortcutKey
|
||||
t = trigger.replace(" ", "")
|
||||
parts = t.split("+")
|
||||
if len(parts) < 2: return None
|
||||
letter = parts[-1].upper()
|
||||
if not (len(letter) == 1 and letter.isalpha()): return None
|
||||
mods = set(p.lower() for p in parts[:-1])
|
||||
has_cmd = ("cmd" in mods) or ("ctrl" in mods)
|
||||
if not has_cmd: return None
|
||||
name = "Ctrl"
|
||||
if "shift" in mods: name += "Shift"
|
||||
if "alt" in mods: name += "Alt"
|
||||
name += letter
|
||||
return getattr(SK, name, None)
|
||||
|
||||
|
||||
def apply_all():
|
||||
"""Liest Defaults + Overrides, wendet alle Aliases + Shortcuts an.
|
||||
Returnt (n_alias, n_fkey, n_cmd, n_skipped)."""
|
||||
global _quit_xml_pairs
|
||||
_quit_xml_pairs = []
|
||||
defaults = _read_defaults()
|
||||
overrides = _read_user_overrides()
|
||||
aliases = Rhino.ApplicationSettings.CommandAliasList
|
||||
skset = Rhino.ApplicationSettings.ShortcutKeySettings
|
||||
n_alias = n_fkey = n_cmd = n_skipped = 0
|
||||
seen_triggers = {} # trigger_normalized -> action_id (Konflikt-Erkennung)
|
||||
|
||||
for action_id, spec in defaults.items():
|
||||
# User-Override hat Vorrang. Leerer String = Default, None/missing = Default.
|
||||
user_trig = overrides.get(action_id)
|
||||
if user_trig is not None and str(user_trig).strip() == "":
|
||||
user_trig = None
|
||||
trigger = user_trig if user_trig else spec.get("trigger", "")
|
||||
if not trigger:
|
||||
n_skipped += 1
|
||||
continue
|
||||
spec_type = spec.get("type", "alias")
|
||||
macro = _expand_macro(spec.get("macro", ""))
|
||||
if not macro:
|
||||
n_skipped += 1; continue
|
||||
|
||||
# Konflikt-Check (gleicher Trigger → letzter gewinnt, Warning)
|
||||
norm = (spec_type, str(trigger).lower())
|
||||
if norm in seen_triggers:
|
||||
print("[ALIASES] Konflikt: '{}' fuer {} bereits von {} belegt"
|
||||
.format(trigger, action_id, seen_triggers[norm]))
|
||||
seen_triggers[norm] = action_id
|
||||
|
||||
try:
|
||||
if spec_type == "alias":
|
||||
tname = str(trigger)
|
||||
try:
|
||||
if aliases.IsAlias(tname):
|
||||
aliases.Delete(tname)
|
||||
except Exception: pass
|
||||
added = False
|
||||
try:
|
||||
added = aliases.Add(tname, macro)
|
||||
except Exception as _addex:
|
||||
print("[ALIASES] Add({}, ...) Exception: {}"
|
||||
.format(tname, _addex))
|
||||
if not added:
|
||||
try: aliases.SetMacro(tname, macro)
|
||||
except Exception: pass
|
||||
# Verifizieren ob Alias wirklich registriert ist
|
||||
try:
|
||||
is_ok = aliases.IsAlias(tname)
|
||||
if not is_ok:
|
||||
print("[ALIASES] WARN: '{}' (action={}) NICHT registriert "
|
||||
"— Rhino lehnt Namen wahrscheinlich ab (z.B. reine Zahl)"
|
||||
.format(tname, action_id))
|
||||
n_skipped += 1
|
||||
continue
|
||||
except Exception: pass
|
||||
n_alias += 1
|
||||
elif spec_type == "fkey":
|
||||
sk = _resolve_fkey(str(trigger))
|
||||
xml_key = _xml_key_from_trigger(str(trigger))
|
||||
api_ok = False
|
||||
if sk is not None:
|
||||
try:
|
||||
skset.SetMacro(sk, macro)
|
||||
got = skset.GetMacro(sk)
|
||||
api_ok = (got == macro)
|
||||
except Exception as _sex:
|
||||
print("[ALIASES] SetMacro({}): {}".format(trigger, _sex))
|
||||
if not api_ok and xml_key:
|
||||
# Enum-Wert fehlt → direkt ins XML (mit verbose-Log).
|
||||
# n_xml=0 kann "schon korrekt" ODER "gescheitert" heissen
|
||||
# — wir checken explizit ob Entry im XML existiert.
|
||||
n_xml = _xml_persist_shortcut(xml_key, macro, verbose=True)
|
||||
if n_xml > 0:
|
||||
_quit_xml_pairs.append((xml_key, macro))
|
||||
else:
|
||||
# n_xml == 0 → entweder "unchanged" (= schon korrekt
|
||||
# im XML) oder "missing path/section". Check via
|
||||
# IsAliasInXml damit wir nicht falsch warnen.
|
||||
if _entry_in_xml(xml_key, macro):
|
||||
# Schon korrekt im XML → fuer Quit-Hook merken
|
||||
# damit Rhino-Quit-Save sie nicht ueberschreibt
|
||||
_quit_xml_pairs.append((xml_key, macro))
|
||||
else:
|
||||
print("[ALIASES] WARN F-Key {} ({}) konnte weder "
|
||||
"API noch XML set werden".format(trigger, action_id))
|
||||
n_skipped += 1; continue
|
||||
n_fkey += 1
|
||||
elif spec_type == "cmd":
|
||||
sk = _resolve_cmd_letter(str(trigger))
|
||||
if sk is None:
|
||||
# Fallback: Cmd+Letter API u.U. nicht im Enum → als Alias mit dem
|
||||
# Letter (single-char) registrieren. User tippt dann Letter+Enter.
|
||||
letter_only = str(trigger).split("+")[-1].lower()
|
||||
if len(letter_only) == 1 and letter_only.isalpha():
|
||||
aliases.SetMacro(letter_only, macro)
|
||||
n_alias += 1
|
||||
print("[ALIASES] {} ({}): Cmd+Letter nicht im Enum, "
|
||||
"fallback Alias '{}'".format(action_id, trigger, letter_only))
|
||||
else:
|
||||
n_skipped += 1
|
||||
continue
|
||||
skset.SetMacro(sk, macro)
|
||||
n_cmd += 1
|
||||
else:
|
||||
print("[ALIASES] Unbekannter Type:", spec_type); n_skipped += 1
|
||||
except Exception as ex:
|
||||
print("[ALIASES] Apply", action_id, "->", trigger, ":", ex)
|
||||
n_skipped += 1
|
||||
|
||||
# Quit-Hook installieren falls XML-only Shortcuts set wurden — diese
|
||||
# ueberlebt sonst Rhino's Auto-Save beim Quit nicht.
|
||||
if _quit_xml_pairs:
|
||||
_install_quit_xml_save(list(_quit_xml_pairs))
|
||||
print("[ALIASES] {} XML-only Shortcuts werden bei Quit "
|
||||
"re-persistiert (closing hook installed)"
|
||||
.format(len(_quit_xml_pairs)))
|
||||
|
||||
return n_alias, n_fkey, n_cmd, n_skipped
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a, f, c, s = apply_all()
|
||||
print("[ALIASES] OK: {} alias, {} fkey, {} cmd, {} skipped"
|
||||
.format(a, f, c, s))
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"_meta": {
|
||||
"version": 2,
|
||||
"description": "DOSSIER Default Shortcuts. Schema: F1-F12 = 2D-Werkzeuge (Single-Tastendruck). Shift+F* = Views/Panels. Cmd+F* = BIM-Objekte. F8/F9 bleiben Rhino-Default (Ortho/Snap). 2D-Tools auch als Alias n1-n0 (Fallback fuer typen). 2-Letter-Aliases (st/tg/ra/sy/sp/dh/au) fuer seltenere BIM. User-Overrides leben in dossier_settings.json unter 'shortcuts_user' = {action_id: trigger_string}. Macro-Platzhalter {ALIASDIR} wird zur Laufzeit ersetzt."
|
||||
},
|
||||
|
||||
"wand": { "type": "fkey", "trigger": "Cmd+F1", "label": "DOSSIER Wand erstellen", "macro": "dWall" },
|
||||
"tuer": { "type": "fkey", "trigger": "Cmd+F2", "label": "DOSSIER Tuer erstellen", "macro": "dDoor" },
|
||||
"fenster": { "type": "fkey", "trigger": "Cmd+F3", "label": "DOSSIER Fenster erstellen", "macro": "dWindow" },
|
||||
"decke": { "type": "fkey", "trigger": "Cmd+F4", "label": "DOSSIER Decke erstellen", "macro": "dSlab" },
|
||||
"treppe": { "type": "fkey", "trigger": "Cmd+F5", "label": "DOSSIER Treppe erstellen", "macro": "dStair" },
|
||||
"stuetze": { "type": "fkey", "trigger": "Cmd+F6", "label": "DOSSIER Stuetze erstellen", "macro": "dColumn" },
|
||||
"traeger": { "type": "fkey", "trigger": "Cmd+F7", "label": "DOSSIER Traeger erstellen", "macro": "dBeam" },
|
||||
"raum": { "type": "fkey", "trigger": "Cmd+F10", "label": "DOSSIER Raum erstellen", "macro": "dRoom" },
|
||||
"symbol": { "type": "fkey", "trigger": "Cmd+F11", "label": "DOSSIER Symbol erstellen", "macro": "dSymbol" },
|
||||
"stempel": { "type": "fkey", "trigger": "Cmd+F12", "label": "DOSSIER Stempel erstellen", "macro": "dTag" },
|
||||
"dach": { "type": "alias", "trigger": "dh", "label": "DOSSIER Dach (Alias)", "macro": "dRoof" },
|
||||
"aussparung": { "type": "alias", "trigger": "au", "label": "DOSSIER Aussparung (Alias)", "macro": "dVoid" },
|
||||
|
||||
"text": { "type": "fkey", "trigger": "F1", "label": "Text", "macro": "_Text" },
|
||||
"line": { "type": "fkey", "trigger": "F2", "label": "Linie", "macro": "_Line" },
|
||||
"arc": { "type": "fkey", "trigger": "F3", "label": "Kreisbogen", "macro": "_Arc" },
|
||||
"rectangle": { "type": "fkey", "trigger": "F4", "label": "Rechteck", "macro": "_Rectangle" },
|
||||
"polyline": { "type": "fkey", "trigger": "F5", "label": "Polylinie", "macro": "_Polyline" },
|
||||
"curve": { "type": "fkey", "trigger": "F6", "label": "Spline / Kurve", "macro": "_Curve" },
|
||||
"hatch": { "type": "fkey", "trigger": "F7", "label": "Schraffur", "macro": "_Hatch" },
|
||||
"polygon": { "type": "fkey", "trigger": "F10", "label": "Polygon", "macro": "_Polygon" },
|
||||
"ellipse": { "type": "fkey", "trigger": "F11", "label": "Ellipse", "macro": "_Ellipse" },
|
||||
"circle": { "type": "fkey", "trigger": "F12", "label": "Kreis", "macro": "_Circle" },
|
||||
|
||||
"view_plan": { "type": "fkey", "trigger": "Cmd+K", "label": "Plan-Mode (Top + Dossier Plan)", "macro": "dPlan" },
|
||||
"view_3d": { "type": "fkey", "trigger": "Cmd+L", "label": "3D-Mode (Perspective + Dossier 3D)", "macro": "d3D" },
|
||||
"zoom_ext": { "type": "fkey", "trigger": "Cmd+U", "label": "Zoom Extents", "macro": "_Zoom _All _Extents" },
|
||||
"zoom_sel": { "type": "fkey", "trigger": "Cmd+Shift+U", "label": "Zoom Selected", "macro": "_Zoom _Selected" },
|
||||
"mod_group": { "type": "fkey", "trigger": "Cmd+G", "label": "Gruppieren (Group)", "macro": "_Group" },
|
||||
"geschoss_up": { "type": "alias", "trigger": "gu", "label": "Geschoss hoch (Alias)", "macro": "dLevelUp" },
|
||||
"geschoss_down": { "type": "fkey", "trigger": "Cmd+B", "label": "Geschoss tief", "macro": "dLevelDown" },
|
||||
"view_material": { "type": "alias", "trigger": "ma", "label": "Material-Mode (Alias)", "macro": "dMaterial" },
|
||||
"panel_layer": { "type": "alias", "trigger": "la", "label": "Layer-Panel (Alias)", "macro": "_Layer" },
|
||||
"panel_elemente": { "type": "alias", "trigger": "el", "label": "DOSSIER Elemente-Panel (Alias)", "macro": "-_ShowPanel \"DOSSIER Elemente\"" },
|
||||
|
||||
"mod_mirror": { "type": "fkey", "trigger": "Cmd+I", "label": "Spiegeln (Mirror)", "macro": "_Mirror" },
|
||||
"mod_copy": { "type": "fkey", "trigger": "Cmd+D", "label": "Kopieren (Copy = Duplicate)", "macro": "_Copy" },
|
||||
"mod_rotate": { "type": "fkey", "trigger": "Cmd+R", "label": "Drehen (Rotate)", "macro": "_Rotate" },
|
||||
"mod_trim": { "type": "fkey", "trigger": "Cmd+T", "label": "Trim (Schneiden)", "macro": "_Trim" },
|
||||
"mod_join": { "type": "fkey", "trigger": "Cmd+J", "label": "Verbinden (Smart-Join: Regionen → Union, sonst Join)", "macro": "dJoin" },
|
||||
"mod_explode": { "type": "fkey", "trigger": "Cmd+E", "label": "Trennen (Explode)", "macro": "_Explode" },
|
||||
"mod_fillet": { "type": "fkey", "trigger": "Cmd+Shift+V", "label": "Verrunden (Fillet)", "macro": "_Fillet" },
|
||||
"mod_move": { "type": "fkey", "trigger": "Cmd+M", "label": "Verschieben (Move)", "macro": "_Move" },
|
||||
"mod_offset": { "type": "fkey", "trigger": "Cmd+Shift+P", "label": "Parallele (OffsetCrv)", "macro": "_OffsetCrv" },
|
||||
"mod_split": { "type": "fkey", "trigger": "Cmd+X", "label": "Smart-Split (Splitlinie zeichnen — ueberschreibt Cut)", "macro": "dSplit" },
|
||||
"mod_chamfer": { "type": "fkey", "trigger": "Cmd+Shift+C", "label": "Abfasen (Chamfer)", "macro": "_Chamfer" },
|
||||
"mod_pipette": { "type": "fkey", "trigger": "Cmd+Y", "label": "Pipette (Einstellungen uebernehmen)", "macro": "dPipette" },
|
||||
"cheatsheet": { "type": "fkey", "trigger": "Cmd+-", "label": "DOSSIER Shortcuts-Cheatsheet", "macro": "dKeys" },
|
||||
|
||||
"text_alias": { "type": "alias", "trigger": "n1", "label": "Text (Alias)", "macro": "_Text" },
|
||||
"line_alias": { "type": "alias", "trigger": "n2", "label": "Linie (Alias)", "macro": "_Line" },
|
||||
"arc_alias": { "type": "alias", "trigger": "n3", "label": "Kreisbogen (Alias)", "macro": "_Arc" },
|
||||
"rectangle_alias": { "type": "alias", "trigger": "n4", "label": "Rechteck (Alias)", "macro": "_Rectangle" },
|
||||
"polyline_alias": { "type": "alias", "trigger": "n5", "label": "Polylinie (Alias)", "macro": "_Polyline" },
|
||||
"curve_alias": { "type": "alias", "trigger": "n6", "label": "Kurve (Alias)", "macro": "_Curve" },
|
||||
"hatch_alias": { "type": "alias", "trigger": "n7", "label": "Schraffur (Alias)", "macro": "_Hatch" },
|
||||
"polygon_alias": { "type": "alias", "trigger": "n8", "label": "Polygon (Alias)", "macro": "_Polygon" },
|
||||
"ellipse_alias": { "type": "alias", "trigger": "n9", "label": "Ellipse (Alias)", "macro": "_Ellipse" },
|
||||
"circle_alias": { "type": "alias", "trigger": "n0", "label": "Kreis (Alias)", "macro": "_Circle" }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Geschoss runter (zum naechsttieferen Eintrag in der Zeichnungsebenen-Liste)
|
||||
import json
|
||||
import scriptcontext as sc
|
||||
import Rhino
|
||||
|
||||
|
||||
def _go(delta):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None:
|
||||
print("[GESCHOSS-NAV] kein Doc"); return
|
||||
bridge = sc.sticky.get("ebenen_bridge_ref")
|
||||
if bridge is None:
|
||||
print("[GESCHOSS-NAV] Ebenen-Bridge nicht aktiv (Panel oeffnen)"); return
|
||||
try:
|
||||
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or ""
|
||||
zs = json.loads(zraw) if zraw else []
|
||||
if not isinstance(zs, list) or not zs:
|
||||
print("[GESCHOSS-NAV] keine Zeichnungsebenen"); return
|
||||
cur_id = doc.Strings.GetValue("dossier_active_id") or ""
|
||||
idx = -1
|
||||
for i, z in enumerate(zs):
|
||||
if isinstance(z, dict) and z.get("id") == cur_id:
|
||||
idx = i; break
|
||||
if idx < 0:
|
||||
idx = len(zs) # nichts aktiv → starten unten
|
||||
new_idx = max(0, min(len(zs) - 1, idx + delta))
|
||||
if new_idx == idx:
|
||||
print("[GESCHOSS-NAV] schon am {}".format(
|
||||
"untersten" if delta > 0 else "obersten")); return
|
||||
target = zs[new_idx]
|
||||
if not isinstance(target, dict) or not target.get("id"):
|
||||
print("[GESCHOSS-NAV] Zielebene ungueltig"); return
|
||||
print("[GESCHOSS-NAV] wechsle zu '{}'".format(target.get("name") or target["id"]))
|
||||
bridge._set_active_zeichnungsebene(target)
|
||||
except Exception as ex:
|
||||
print("[GESCHOSS-NAV]", ex)
|
||||
|
||||
|
||||
# delta=+1 = nach unten (naechster Eintrag in der Liste)
|
||||
_go(+1)
|
||||
@@ -0,0 +1,43 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Geschoss hoch (zum naechstoberen Eintrag in der Zeichnungsebenen-Liste)
|
||||
import json
|
||||
import scriptcontext as sc
|
||||
import Rhino
|
||||
|
||||
|
||||
def _go(delta):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None:
|
||||
print("[GESCHOSS-NAV] kein Doc"); return
|
||||
bridge = sc.sticky.get("ebenen_bridge_ref")
|
||||
if bridge is None:
|
||||
print("[GESCHOSS-NAV] Ebenen-Bridge nicht aktiv (Panel oeffnen)"); return
|
||||
try:
|
||||
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or ""
|
||||
zs = json.loads(zraw) if zraw else []
|
||||
if not isinstance(zs, list) or not zs:
|
||||
print("[GESCHOSS-NAV] keine Zeichnungsebenen"); return
|
||||
cur_id = doc.Strings.GetValue("dossier_active_id") or ""
|
||||
idx = -1
|
||||
for i, z in enumerate(zs):
|
||||
if isinstance(z, dict) and z.get("id") == cur_id:
|
||||
idx = i; break
|
||||
if idx < 0:
|
||||
idx = 0 # nichts aktiv → starten oben
|
||||
new_idx = max(0, min(len(zs) - 1, idx + delta))
|
||||
if new_idx == idx:
|
||||
print("[GESCHOSS-NAV] schon am {}".format(
|
||||
"obersten" if delta < 0 else "untersten")); return
|
||||
target = zs[new_idx]
|
||||
if not isinstance(target, dict) or not target.get("id"):
|
||||
print("[GESCHOSS-NAV] Zielebene ungueltig"); return
|
||||
print("[GESCHOSS-NAV] wechsle zu '{}'".format(target.get("name") or target["id"]))
|
||||
bridge._set_active_zeichnungsebene(target)
|
||||
except Exception as ex:
|
||||
print("[GESCHOSS-NAV]", ex)
|
||||
|
||||
|
||||
# delta=-1 = nach oben (vorheriger Eintrag in der Liste, weil Listen
|
||||
# typischerweise oberste Ebene oben sind)
|
||||
_go(-1)
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer View-Mode 'material'.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_view_mode
|
||||
dossier_view_mode._apply("material")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer View-Mode 'persp3d'.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_view_mode
|
||||
dossier_view_mode._apply("persp3d")
|
||||
@@ -0,0 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Auto-Wrapper fuer View-Mode 'plan'.
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import dossier_view_mode
|
||||
dossier_view_mode._apply("plan")
|
||||
+207
-27
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
ausschnitte.py
|
||||
AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode,
|
||||
@@ -147,9 +149,9 @@ def _apply_camera(vp, cam):
|
||||
Rhino.RhinoApp.RunScript(
|
||||
"_-Zoom _Factor {:.6f} _Enter".format(factor), False)
|
||||
except Exception as ex:
|
||||
print("[AUSSCHNITTE] Frustum-Apply:", ex)
|
||||
print("[VIEWPORTS] Frustum-Apply:", ex)
|
||||
except Exception as ex:
|
||||
print("[AUSSCHNITTE] Camera-Apply:", ex)
|
||||
print("[VIEWPORTS] Camera-Apply:", ex)
|
||||
|
||||
|
||||
def _capture_layers(doc):
|
||||
@@ -241,7 +243,7 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
|
||||
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)
|
||||
print("[VIEWPORTS] apply_to_detail: snap not found", snap_id)
|
||||
return False
|
||||
# Page-View ermitteln (fuer SetActiveDetail/SetPageAsActive)
|
||||
page_view = None
|
||||
@@ -255,14 +257,14 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as ex:
|
||||
print("[AUSSCHNITTE] page-view-suche:", ex)
|
||||
print("[VIEWPORTS] 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)
|
||||
except Exception as ex: print("[VIEWPORTS] SetActiveDetail:", ex)
|
||||
# Kamera + Layer + Name
|
||||
vp = detail.Viewport
|
||||
_apply_camera(vp, snap.get("camera"))
|
||||
@@ -272,7 +274,7 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
|
||||
if new_name and vp.Name != new_name:
|
||||
vp.Name = new_name
|
||||
except Exception as ex:
|
||||
print("[AUSSCHNITTE] Detail-Rename:", ex)
|
||||
print("[VIEWPORTS] Detail-Rename:", ex)
|
||||
# Massstab
|
||||
ratio = _parse_scale(snap.get("scale", ""))
|
||||
if ratio is not None:
|
||||
@@ -298,7 +300,7 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
|
||||
(page_view or doc.Views).Redraw()
|
||||
except Exception:
|
||||
doc.Views.Redraw()
|
||||
print("[AUSSCHNITTE] '{}' auf Detail {} angewendet".format(snap.get("name"), detail.Id))
|
||||
print("[VIEWPORTS] '{}' auf Detail {} applied".format(snap.get("name"), detail.Id))
|
||||
return True
|
||||
|
||||
|
||||
@@ -327,6 +329,8 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
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 == "SET_DARSTELLUNG": self._set_field(p.get("id"), "darstellung",
|
||||
p.get("darstellung") 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"))
|
||||
@@ -334,6 +338,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
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"))
|
||||
elif t == "OPEN_SETTINGS": self._open_settings_window(p.get("id"))
|
||||
|
||||
def _load(self, doc):
|
||||
raw = doc.Strings.GetValue(_STORE_KEY)
|
||||
@@ -445,7 +450,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
snaps.insert(idx + 1, copy)
|
||||
self._store(doc, snaps)
|
||||
self._send_list()
|
||||
print("[AUSSCHNITTE] '{}' dupliziert".format(src.get("name")))
|
||||
print("[VIEWPORTS] '{}' dupliziert".format(src.get("name")))
|
||||
|
||||
def _set_field(self, snap_id, field, value):
|
||||
if not snap_id: return
|
||||
@@ -461,7 +466,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
def _capture(self, doc, name, existing_id=None, prior_scale=""):
|
||||
view = doc.Views.ActiveView
|
||||
if view is None:
|
||||
print("[AUSSCHNITTE] Keine aktive View")
|
||||
print("[VIEWPORTS] Keine aktive View")
|
||||
return None
|
||||
vp = view.ActiveViewport
|
||||
# Aktuelle Skala vom MASSSTAB-Modul holen — nur sinnvoll bei Parallel-
|
||||
@@ -478,7 +483,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
else:
|
||||
scale_str = "1:{:.1f}".format(ratio)
|
||||
except Exception as ex:
|
||||
print("[AUSSCHNITTE] Live-Skala lesen:", ex)
|
||||
print("[VIEWPORTS] 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
|
||||
@@ -490,9 +495,16 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
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)
|
||||
print("[VIEWPORTS] Live-Skala (Fallback):", ex)
|
||||
if not scale_str and prior_scale:
|
||||
scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben
|
||||
# Darstellungs-Override aus dem aktuellen Doc-Setting uebernehmen.
|
||||
# Leer = "kein Override, per-Object respektieren".
|
||||
darst = ""
|
||||
try:
|
||||
import elemente
|
||||
darst = elemente.get_aktive_darstellung(doc) or ""
|
||||
except Exception: pass
|
||||
return {
|
||||
"id": existing_id or "snap_" + uuid.uuid4().hex[:8],
|
||||
"name": name,
|
||||
@@ -500,6 +512,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
"camera": _capture_camera(vp),
|
||||
"layers": _capture_layers(doc),
|
||||
"dossier": _capture_dossier_state(doc),
|
||||
"darstellung": darst,
|
||||
}
|
||||
|
||||
def _save(self, name):
|
||||
@@ -510,7 +523,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
snaps.append(snap)
|
||||
self._store(doc, snaps)
|
||||
self._send_list()
|
||||
print("[AUSSCHNITTE] '{}' gespeichert".format(name))
|
||||
print("[VIEWPORTS] '{}' gespeichert".format(name))
|
||||
|
||||
def _update(self, snap_id):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
@@ -527,7 +540,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
break
|
||||
self._store(doc, snaps)
|
||||
self._send_list()
|
||||
print("[AUSSCHNITTE] '{}' aktualisiert".format(target.get("name")))
|
||||
print("[VIEWPORTS] '{}' aktualisiert".format(target.get("name")))
|
||||
|
||||
def _restore(self, snap_id):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
@@ -537,8 +550,57 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
if view is None: return
|
||||
vp = view.ActiveViewport
|
||||
_apply_camera(vp, snap.get("camera"))
|
||||
# Layer-Sichtbarkeit: bevorzugt die referenzierte Ebenenkombi (live —
|
||||
# zeigt aktuelle Kombi-Definition). Fallback: snap.layers (per-snap
|
||||
# eingefrorener Zustand).
|
||||
kombi = (snap.get("layerCombination") or "").strip()
|
||||
if kombi:
|
||||
try:
|
||||
import layers_panel as rhinopanel
|
||||
rhinopanel.apply_layer_preset_by_name(doc, kombi)
|
||||
except Exception as ex:
|
||||
print("[VIEWPORTS] kombi-apply '{}':".format(kombi), ex)
|
||||
_apply_layers_global(doc, snap.get("layers", []))
|
||||
else:
|
||||
_apply_layers_global(doc, snap.get("layers", []))
|
||||
# Eigene Sichtbarkeit → active_comb_name clearen
|
||||
try:
|
||||
import layers_panel as rhinopanel
|
||||
rhinopanel.set_active_comb_name(doc, None)
|
||||
rhinopanel._notify_oberleiste_combs()
|
||||
except Exception: pass
|
||||
_apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {})
|
||||
# Darstellung anwenden + Oeffnungen regenerieren
|
||||
try:
|
||||
import elemente
|
||||
new_darst = snap.get("darstellung") or ""
|
||||
cur_darst = elemente.get_aktive_darstellung(doc) or ""
|
||||
if new_darst != cur_darst:
|
||||
elemente.set_aktive_darstellung(doc, new_darst)
|
||||
elemente.regenerate_all_oeffnungen(doc)
|
||||
# Oberleiste-Topbar muss neuen Wert spiegeln
|
||||
try:
|
||||
b = sc.sticky.get("oberleiste_bridge")
|
||||
if b is not None: b._send_state(force=True)
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[VIEWPORTS] darstellung apply:", ex)
|
||||
# Overrides: nur anwenden wenn das Snap "applyOverrides" set hat.
|
||||
# Sonst bleibt der aktuelle User-Override-State unangetastet.
|
||||
if snap.get("applyOverrides"):
|
||||
try:
|
||||
import overrides
|
||||
overrides.set_enabled(doc, bool(snap.get("overridesEnabled")))
|
||||
overrides.set_active_preset(doc, snap.get("overridesPreset") or None)
|
||||
# Oberleiste-Cache invalidieren damit Topbar das neue Preset zeigt
|
||||
try:
|
||||
b = sc.sticky.get("oberleiste_bridge")
|
||||
if b is not None:
|
||||
b._cached_overrides = None
|
||||
b._send_state(force=True)
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[VIEWPORTS] overrides-apply:", ex)
|
||||
# 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
|
||||
@@ -548,8 +610,8 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
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
|
||||
print("[VIEWPORTS] Rename:", ex)
|
||||
# Gespeicherten Massstab anwenden (z.B. "1:50") — falls present und
|
||||
# Viewport parallel ist (in Perspective ignoriert massstab._apply_scale).
|
||||
try:
|
||||
scale_str = (snap.get("scale") or "").strip()
|
||||
@@ -559,7 +621,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
_, 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(
|
||||
print("[VIEWPORTS] Massstab set 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
|
||||
@@ -568,15 +630,15 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
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"))
|
||||
print("[VIEWPORTS] 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))
|
||||
print("[VIEWPORTS] force-send {} failed: {}".format(key, e))
|
||||
except Exception as ex:
|
||||
print("[AUSSCHNITTE] Massstab-Restore:", ex)
|
||||
print("[VIEWPORTS] Massstab-Restore:", ex)
|
||||
view.Redraw()
|
||||
print("[AUSSCHNITTE] '{}' wiederhergestellt".format(snap.get("name")))
|
||||
print("[VIEWPORTS] '{}' wiederhergestellt".format(snap.get("name")))
|
||||
|
||||
def _apply_to_detail(self, snap_id):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
@@ -591,9 +653,9 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
detail = d
|
||||
break
|
||||
except Exception as ex:
|
||||
print("[AUSSCHNITTE] Active-Detail-Suche:", ex)
|
||||
print("[VIEWPORTS] Active-Detail-Suche:", ex)
|
||||
if detail is None:
|
||||
print("[AUSSCHNITTE] Kein Detail ausgewaehlt — bitte:")
|
||||
print("[VIEWPORTS] 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")
|
||||
@@ -606,7 +668,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
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)
|
||||
print("[VIEWPORTS] Snap not found:", snap_id)
|
||||
return
|
||||
snap_by_id = {}
|
||||
for ls in (snap.get("layers") or []):
|
||||
@@ -652,7 +714,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
target["layers"] = new_list
|
||||
self._store(doc, snaps)
|
||||
self._send_list()
|
||||
print("[AUSSCHNITTE] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name")))
|
||||
print("[VIEWPORTS] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name")))
|
||||
|
||||
def _save_preset(self, name, layers):
|
||||
if not name: return
|
||||
@@ -676,7 +738,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
presets.append({"name": name, "layers": clean})
|
||||
self._store_presets(doc, presets)
|
||||
self._send_list()
|
||||
print("[AUSSCHNITTE] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean)))
|
||||
print("[VIEWPORTS] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean)))
|
||||
|
||||
def _delete_preset(self, name):
|
||||
if not name: return
|
||||
@@ -703,6 +765,124 @@ class AusschnittBridge(panel_base.BaseBridge):
|
||||
self._store(doc, snaps)
|
||||
self._send_list()
|
||||
|
||||
def _open_settings_window(self, snap_id):
|
||||
"""Oeffnet ein Satelliten-Fenster (Eto.Form + WebView) mit dem
|
||||
Ausschnittseinstellungen-Dialog. Lets User editieren: Massstab,
|
||||
Display-Mode, Overrides, Ebenenkombi."""
|
||||
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("[VIEWPORTS] open_settings: snap not found", snap_id)
|
||||
return
|
||||
outer = self
|
||||
bridge_holder = {"form": None, "id": snap_id}
|
||||
|
||||
panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge,
|
||||
icon_spec=("A", "#c87050"))
|
||||
def _payload():
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
sn = next((s for s in outer._load(d) if s.get("id") == bridge_holder["id"]), None)
|
||||
if sn is None: sn = {}
|
||||
# Listen fuer Dropdowns
|
||||
display_modes = []
|
||||
try:
|
||||
import toolbar as oberleiste
|
||||
display_modes = oberleiste._list_display_modes()
|
||||
except Exception as ex:
|
||||
print("[VIEWPORTS] display_modes:", ex)
|
||||
overrides_presets = []
|
||||
try:
|
||||
import overrides
|
||||
overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")]
|
||||
except Exception as ex:
|
||||
print("[VIEWPORTS] overrides_presets:", ex)
|
||||
layer_kombis = []
|
||||
try:
|
||||
import layers_panel as rhinopanel
|
||||
layer_kombis = rhinopanel.list_layer_preset_names(d)
|
||||
except Exception as ex:
|
||||
print("[VIEWPORTS] layer_kombis:", ex)
|
||||
cam = sn.get("camera") or {}
|
||||
return {
|
||||
"snap": {
|
||||
"id": sn.get("id"),
|
||||
"name": sn.get("name"),
|
||||
"scale": sn.get("scale", ""),
|
||||
"displayMode": cam.get("displayMode"),
|
||||
"displayModeName": cam.get("displayModeName"),
|
||||
"applyOverrides": bool(sn.get("applyOverrides", False)),
|
||||
"overridesEnabled": bool(sn.get("overridesEnabled", False)),
|
||||
"overridesPreset": sn.get("overridesPreset") or "",
|
||||
"layerCombination": sn.get("layerCombination") or "",
|
||||
"darstellung": sn.get("darstellung") or "",
|
||||
},
|
||||
"displayModes": display_modes,
|
||||
"overridesPresets": overrides_presets,
|
||||
"layerKombis": layer_kombis,
|
||||
}
|
||||
|
||||
def _persist(settings):
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
snaps = outer._load(d)
|
||||
sid = bridge_holder["id"]
|
||||
target = next((s for s in snaps if s.get("id") == sid), None)
|
||||
if target is None:
|
||||
print("[VIEWPORTS] persist settings: snap weg"); return
|
||||
# Massstab
|
||||
sc_val = (settings.get("scale") or "").strip()
|
||||
target["scale"] = sc_val
|
||||
# Display Mode in camera nested
|
||||
cam = target.get("camera") or {}
|
||||
dm_id = settings.get("displayMode")
|
||||
dm_nm = settings.get("displayModeName")
|
||||
if dm_id is not None: cam["displayMode"] = dm_id or None
|
||||
if dm_nm is not None: cam["displayModeName"] = dm_nm or None
|
||||
target["camera"] = cam
|
||||
# Overrides
|
||||
target["applyOverrides"] = bool(settings.get("applyOverrides"))
|
||||
target["overridesEnabled"] = bool(settings.get("overridesEnabled"))
|
||||
target["overridesPreset"] = (settings.get("overridesPreset") or "").strip()
|
||||
# Ebenenkombi
|
||||
target["layerCombination"] = (settings.get("layerCombination") or "").strip()
|
||||
# Darstellung (SIA-400 LoD Override fuer diesen Ausschnitt)
|
||||
darst = (settings.get("darstellung") or "").strip()
|
||||
target["darstellung"] = darst if darst in ("einfach", "standard", "detail") else ""
|
||||
outer._store(d, snaps)
|
||||
outer._send_list()
|
||||
print("[VIEWPORTS] Settings fuer '{}' aktualisiert".format(target.get("name")))
|
||||
|
||||
class _AusschnittSettingsBridge(panel_base.BaseBridge):
|
||||
def __init__(self):
|
||||
panel_base.BaseBridge.__init__(self, "ausschnitt_settings")
|
||||
def _on_ready(self):
|
||||
self._send_state()
|
||||
def _send_state(self):
|
||||
self.send("AUSSCHNITT_SETTINGS_STATE", _payload())
|
||||
def handle(self, data):
|
||||
if not isinstance(data, dict): return
|
||||
t = data.get("type", "")
|
||||
p = data.get("payload") or {}
|
||||
if t == "READY":
|
||||
self._on_ready()
|
||||
elif t == "SAVE":
|
||||
_persist(p.get("settings") or {})
|
||||
try:
|
||||
f = bridge_holder.get("form")
|
||||
if f is not None: f.Close()
|
||||
except Exception: pass
|
||||
elif t == "CANCEL":
|
||||
try:
|
||||
f = bridge_holder.get("form")
|
||||
if f is not None: f.Close()
|
||||
except Exception: pass
|
||||
|
||||
b = _AusschnittSettingsBridge()
|
||||
bridge_holder["form"] = panel_base.open_satellite_window(
|
||||
"ausschnitt_settings",
|
||||
params=_payload(),
|
||||
title="Ausschnitt: {}".format(snap.get("name", "")),
|
||||
size=(420, 540),
|
||||
bridge=b)
|
||||
|
||||
|
||||
panel_base.register_and_open("ausschnitte", "Ausschnitte", PANEL_GUID_STR, AusschnittBridge,
|
||||
icon_spec=("crop", "#c87050"))
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
begin_cmd_hook.py
|
||||
Hook auf Rhino.Commands.Command.BeginCommand. Wenn der User ein Drawing-
|
||||
Command startet (Line, Polyline, Rectangle, Circle etc.), oeffnen wir
|
||||
automatisch das DOSSIER-Gestaltung-Panel und bringen es in den Vordergrund.
|
||||
|
||||
Idempotent — Re-Install nach _reset_panels deregistriert alten Handler.
|
||||
"""
|
||||
import Rhino
|
||||
import scriptcontext as sc
|
||||
import System
|
||||
|
||||
|
||||
# Commands bei denen wir Gestaltung-Panel fokussieren.
|
||||
# CommandEnglishName ohne Underscore-Prefix.
|
||||
_DRAWING_COMMANDS = {
|
||||
"Line", "Polyline", "Curve", "InterpCrv",
|
||||
"Arc", "Circle", "Ellipse",
|
||||
"Rectangle", "Polygon",
|
||||
"Hatch", "Text",
|
||||
"Point", "Points",
|
||||
"InfiniteLine",
|
||||
}
|
||||
|
||||
_GESTALTUNG_PANEL_GUID = "4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829"
|
||||
_HANDLER_KEY = "_dossier_begin_cmd_handler"
|
||||
_VERBOSE_KEY = "_dossier_begin_cmd_verbose"
|
||||
|
||||
|
||||
def _on_begin_command(sender, e):
|
||||
try:
|
||||
cmd = getattr(e, "CommandEnglishName", "") or ""
|
||||
if sc.sticky.get(_VERBOSE_KEY):
|
||||
print("[CMD-HOOK] cmd='{}'".format(cmd))
|
||||
if cmd not in _DRAWING_COMMANDS: return
|
||||
try:
|
||||
guid = System.Guid(_GESTALTUNG_PANEL_GUID)
|
||||
Rhino.UI.Panels.OpenPanel(guid)
|
||||
try:
|
||||
Rhino.UI.Panels.FocusPanel(guid)
|
||||
except Exception: pass
|
||||
if sc.sticky.get(_VERBOSE_KEY):
|
||||
print("[CMD-HOOK] Gestaltung-Panel opened/focused")
|
||||
except Exception as ex:
|
||||
print("[CMD-HOOK] OpenPanel:", ex)
|
||||
try:
|
||||
Rhino.RhinoApp.RunScript(
|
||||
'-_ShowPanel "DOSSIER Gestaltung"', False)
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[CMD-HOOK] handler:", ex)
|
||||
|
||||
|
||||
def install(verbose=False):
|
||||
"""Einmalige Registrierung. Bei Re-Install (z.B. nach _reset_panels)
|
||||
wird der alte Handler-Ref aus sc.sticky deregistriert."""
|
||||
old = sc.sticky.get(_HANDLER_KEY)
|
||||
if old is not None:
|
||||
try: Rhino.Commands.Command.BeginCommand -= old
|
||||
except Exception: pass
|
||||
try:
|
||||
Rhino.Commands.Command.BeginCommand += _on_begin_command
|
||||
sc.sticky[_HANDLER_KEY] = _on_begin_command
|
||||
sc.sticky[_VERBOSE_KEY] = bool(verbose)
|
||||
print("[CMD-HOOK] Hook installed (verbose={})".format(bool(verbose)))
|
||||
except Exception as ex:
|
||||
print("[CMD-HOOK] install:", ex)
|
||||
|
||||
|
||||
def set_verbose(flag):
|
||||
sc.sticky[_VERBOSE_KEY] = bool(flag)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
install(verbose=True)
|
||||
@@ -1,4 +1,6 @@
|
||||
#! python 3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
clean.py
|
||||
Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#! python 3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
clean_layers.py
|
||||
Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.)
|
||||
@@ -48,4 +50,4 @@ 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")
|
||||
print("[clean_layers] Panel-Sticky zurueckset")
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
curve_vertex_dots.py
|
||||
Display-only Vertex-Dots fuer GENERISCHE Curves (Polylinen, Linien,
|
||||
Rectangles, NurbsCurves etc). Zeigt gruene Punkte an allen Vertices
|
||||
selektierter Curves — hilft beim Visuell-Finden von Grip-Positionen
|
||||
wenn die Curve eine Fuellung (Hatch) hat und schwer per Klick auf
|
||||
einen einzelnen Vertex zu treffen ist.
|
||||
|
||||
Display-only — kein eigener Drag-Handler. User editiert Vertices via
|
||||
Rhino's native _Grips (Punkte sichtbar machen + Standard-Drag) oder
|
||||
direktes Object-Snapping waehrend Drag.
|
||||
|
||||
Skipt dossier-managed Curves (wand_axis, treppe_axis, schnitt_axis,
|
||||
wand_outline, wand_centerline, raum_polylinie etc) — die haben ihre
|
||||
eigenen Conduits oder duerfen nicht via Vertex editiert werden.
|
||||
"""
|
||||
import Rhino
|
||||
import Rhino.Display as rd
|
||||
import Rhino.Geometry as rg
|
||||
import scriptcontext as sc
|
||||
import System.Drawing as SD
|
||||
|
||||
|
||||
# --- Konstanten ------------------------------------------------------------
|
||||
|
||||
_MARKER_RADIUS_PX = 6
|
||||
_MARKER_FILL = SD.Color.FromArgb(200, 95, 168, 150) # accent-gruen
|
||||
_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
|
||||
|
||||
# Dossier-managed Element-Types die NICHT mit generic dots versehen werden
|
||||
# (= haben eigene Conduits oder sind nicht editierbar via Vertex-Click).
|
||||
_SKIP_TYPES = {
|
||||
"wand_axis", "wand_centerline", "wand_outline", "wand_volume",
|
||||
"treppe_axis", "treppe_outline", "treppe_volume",
|
||||
"schnitt_axis", "schnitt_outline",
|
||||
"raum_polylinie", "raum_stempel",
|
||||
"ausschnitt_polylinie",
|
||||
"decke_polylinie", "decke_volume",
|
||||
"dach_polylinie", "dach_volume",
|
||||
}
|
||||
|
||||
|
||||
# --- Helpers --------------------------------------------------------------
|
||||
|
||||
def _is_dossier_managed(obj):
|
||||
"""True wenn obj ein dossier-managed Element ist (= Skip)."""
|
||||
if obj is None or obj.IsDeleted: return True
|
||||
try:
|
||||
t = obj.Attributes.GetUserString("dossier_element_type") or ""
|
||||
return t in _SKIP_TYPES
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _curve_vertices(curve):
|
||||
"""Liefert Liste von rg.Point3d fuer alle relevanten Vertices der
|
||||
Curve. Verschiedene Curve-Types haben verschiedene Vertices:
|
||||
- LineCurve: 2 Endpunkte
|
||||
- PolylineCurve: alle Polyline-Punkte (deduplizert wenn closed)
|
||||
- PolyCurve: rekursiv Segmente
|
||||
- NurbsCurve/sonst: Start + End (control points nicht — zu viele)"""
|
||||
pts = []
|
||||
if curve is None: return pts
|
||||
try:
|
||||
if isinstance(curve, rg.PolylineCurve):
|
||||
ok, pline = curve.TryGetPolyline()
|
||||
if ok and pline is not None:
|
||||
n = pline.Count
|
||||
# Deduplizieren wenn closed (letzter Punkt = erster)
|
||||
last = n
|
||||
try:
|
||||
if (n >= 2
|
||||
and pline[0].DistanceTo(pline[n - 1]) < 1e-6):
|
||||
last = n - 1
|
||||
except Exception: pass
|
||||
for i in range(last):
|
||||
pts.append(rg.Point3d(pline[i]))
|
||||
return pts
|
||||
if isinstance(curve, rg.LineCurve):
|
||||
pts.append(curve.PointAtStart)
|
||||
pts.append(curve.PointAtEnd)
|
||||
return pts
|
||||
if isinstance(curve, rg.PolyCurve):
|
||||
for i in range(curve.SegmentCount):
|
||||
seg = curve.SegmentCurve(i)
|
||||
if seg is None: continue
|
||||
# Nur Start jedes Segments (End ist Start des naechsten)
|
||||
pts.append(seg.PointAtStart)
|
||||
# Letztes Segment-End anhaengen
|
||||
try:
|
||||
pts.append(curve.PointAtEnd)
|
||||
except Exception: pass
|
||||
return pts
|
||||
# Generic Curve: nur Start + End
|
||||
try:
|
||||
pts.append(curve.PointAtStart)
|
||||
pts.append(curve.PointAtEnd)
|
||||
except Exception: pass
|
||||
except Exception:
|
||||
pass
|
||||
return pts
|
||||
|
||||
|
||||
# --- Conduit -------------------------------------------------------------
|
||||
|
||||
class _VertexDotConduit(rd.DisplayConduit):
|
||||
"""Zeichnet bei jeder selektierten generischen Curve gruene Punkte
|
||||
an allen Vertices."""
|
||||
|
||||
def DrawForeground(self, e):
|
||||
try:
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
try:
|
||||
sel = list(doc.Objects.GetSelectedObjects(False, False))
|
||||
except Exception: return
|
||||
seen_curve_ids = set()
|
||||
for obj in sel:
|
||||
if _is_dossier_managed(obj): continue
|
||||
try:
|
||||
cid = str(obj.Id)
|
||||
except Exception: continue
|
||||
if cid in seen_curve_ids: continue
|
||||
seen_curve_ids.add(cid)
|
||||
geom = obj.Geometry
|
||||
if not isinstance(geom, rg.Curve): continue
|
||||
for pt in _curve_vertices(geom):
|
||||
try:
|
||||
e.Display.DrawPoint(
|
||||
pt, rd.PointStyle.RoundControlPoint,
|
||||
_MARKER_RADIUS_PX, _MARKER_FILL)
|
||||
except Exception:
|
||||
try: e.Display.DrawDot(
|
||||
pt, "·", _MARKER_FILL, _MARKER_BORDER)
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[CURVE-DOTS] DrawForeground:", ex)
|
||||
|
||||
|
||||
# --- Install -------------------------------------------------------------
|
||||
|
||||
_STICKY_CONDUIT = "_dossier_curve_vertex_dots_conduit"
|
||||
|
||||
|
||||
def install_curve_vertex_dots():
|
||||
"""Idempotent: alten Conduit disable, neuen installieren."""
|
||||
try:
|
||||
old = sc.sticky.get(_STICKY_CONDUIT)
|
||||
if old is not None:
|
||||
try: old.Enabled = False
|
||||
except Exception: pass
|
||||
conduit = _VertexDotConduit()
|
||||
conduit.Enabled = True
|
||||
sc.sticky[_STICKY_CONDUIT] = conduit
|
||||
print("[CURVE-DOTS] Vertex dot conduit active")
|
||||
except Exception as ex:
|
||||
print("[CURVE-DOTS] install:", ex)
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
dimensionen.py
|
||||
DIMENSIONEN-Panel: Object Info Palette nach Vectorworks-Vorbild.
|
||||
@@ -181,7 +183,7 @@ def _apply_xform(doc, objs, xform):
|
||||
if doc.Objects.Transform(obj.Id, xform, True):
|
||||
n += 1
|
||||
except Exception as ex:
|
||||
print("[DIMENSIONEN] Transform-Fehler:", ex)
|
||||
print("[DIMENSIONS] Transform-Fehler:", ex)
|
||||
return n
|
||||
|
||||
|
||||
@@ -199,14 +201,14 @@ class _UndoRecord(object):
|
||||
try:
|
||||
self.serial = self.doc.BeginUndoRecord(self.label)
|
||||
except Exception as ex:
|
||||
print("[DIMENSIONEN] BeginUndoRecord:", ex)
|
||||
print("[DIMENSIONS] 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)
|
||||
print("[DIMENSIONS] EndUndoRecord:", ex)
|
||||
return False # exceptions propagieren
|
||||
|
||||
|
||||
@@ -225,7 +227,7 @@ def _scale_around_point(doc, objs, plane, ref_world, sx, sy, sz):
|
||||
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)
|
||||
print("[DIMENSIONS] Ungueltige Skalierungsfaktoren:", sx, sy, sz)
|
||||
return
|
||||
p = rg.Plane(plane)
|
||||
p.Origin = ref_world
|
||||
@@ -580,12 +582,32 @@ def _install_listeners(bridge):
|
||||
return
|
||||
|
||||
def on_idle(s, e):
|
||||
# Waehrend Bulk-Ops (z.B. _Delete bei 6000 Objekten): nicht pollen.
|
||||
# tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem
|
||||
# Tick zwischen den einzelnen Deletes. CommandEnd refresht.
|
||||
if sc.sticky.get("_dossier_bulk_op_active"): return
|
||||
# Waehrend Gumball/Move/Rotate: nicht pollen. Geometrie ist gerade
|
||||
# in Transit (Live-Replace pro Frame), Werte wuerden mit ~5/s
|
||||
# zwischen Frames flickern. CommandEnd triggert finalen _send_state.
|
||||
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||
b = sc.sticky.get("dimensionen_bridge")
|
||||
if b is not None:
|
||||
try: b.tick_idle()
|
||||
except Exception as ex: print("[DIMENSIONEN] idle:", ex)
|
||||
except Exception as ex: print("[DIMENSIONS] idle:", ex)
|
||||
|
||||
def on_select(s, e):
|
||||
# Swisstopo-Import feuert tausende Selection-Events → bail.
|
||||
if sc.sticky.get("dossier_swisstopo_busy"): return
|
||||
if sc.sticky.get("_dossier_bulk_op_active"): return
|
||||
# Waehrend elemente.py's Partnership-Cascade (Klick auf Wand/Treppe
|
||||
# → 30+ Partner selektiert in einem Rutsch): NICHT pro Event ein
|
||||
# _send_state feuern. Sonst rauscht das Dimensionen-Panel mit 30+
|
||||
# Re-Renders durch und die Werte/Auswahl-Anzeige flickert wild.
|
||||
# Der Idle-Tick holt die finale Selektion eh ~5/s nach.
|
||||
if sc.sticky.get("_elemente_select_busy"): return
|
||||
# Waehrend User-Transform (Gumball/Move/Rotate): kein Re-Send, sonst
|
||||
# rauscht Replace-Storm durch und der Frontend-State zappelt.
|
||||
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||
b = sc.sticky.get("dimensionen_bridge")
|
||||
if b is not None:
|
||||
try: b._send_state(force=True)
|
||||
@@ -597,9 +619,9 @@ def _install_listeners(bridge):
|
||||
Rhino.RhinoDoc.DeselectObjects += on_select
|
||||
Rhino.RhinoDoc.DeselectAllObjects += on_select
|
||||
except Exception as ex:
|
||||
print("[DIMENSIONEN] select-events:", ex)
|
||||
print("[DIMENSIONS] select-events:", ex)
|
||||
sc.sticky[flag] = True
|
||||
print("[DIMENSIONEN] Listener aktiv (Idle + SelectObjects)")
|
||||
print("[DIMENSIONS] Listener active (Idle + SelectObjects)")
|
||||
|
||||
|
||||
def _bridge_factory():
|
||||
@@ -608,5 +630,6 @@ def _bridge_factory():
|
||||
return b
|
||||
|
||||
|
||||
panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR,
|
||||
_bridge_factory, icon_spec=("D", "#9e7050"))
|
||||
panel_base.register_and_open("dimensionen", "Dimensionen", PANEL_GUID_STR,
|
||||
_bridge_factory,
|
||||
icon_spec=("aspect_ratio", "#9e7050"))
|
||||
+14107
-379
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
elemente_properties.py
|
||||
Properties-Satellite-Window. Zeigt die Property-Forms (WallProperties,
|
||||
RaumProperties, etc.) in einem eigenen groesseren Fenster — fuer Power-
|
||||
User die mehr Platz beim Editieren wollen ohne dass das Elemente-Panel
|
||||
ueberfrachtet wird. Daten kommen 1:1 vom ElementeBridge (sticky).
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
class ElementePropertiesBridge(panel_base.BaseBridge):
|
||||
def __init__(self):
|
||||
panel_base.BaseBridge.__init__(self, "elemente_properties")
|
||||
|
||||
def _send_state(self):
|
||||
# Holt sich den aktuellen State vom Haupt-ElementeBridge — der hat
|
||||
# die Element-Enumeration + Selection-Erkennung schon implementiert.
|
||||
elemente_bridge = sc.sticky.get("elemente_bridge")
|
||||
if elemente_bridge is None:
|
||||
self.send("STATE", {"elements": [], "geschosse": [], "selection": None})
|
||||
return
|
||||
try:
|
||||
elemente_bridge._send_state() # broadcast — auch wir bekommen das via sticky-Forward
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE-PROPS] _send_state fail:", ex)
|
||||
|
||||
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" or t == "REQUEST_STATE":
|
||||
self._on_ready()
|
||||
elif t == "UPDATE_ELEMENT" or t == "DELETE_ELEMENT":
|
||||
# Forward to main ElementeBridge — same handler
|
||||
eb = sc.sticky.get("elemente_bridge")
|
||||
if eb is not None:
|
||||
try: eb.handle(data)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE-PROPS] forward:", ex)
|
||||
|
||||
|
||||
def open_as_window():
|
||||
"""Oeffnet die Properties-View als Satellite-Window."""
|
||||
b = ElementePropertiesBridge()
|
||||
sc.sticky["elemente_properties_bridge"] = b
|
||||
panel_base.open_satellite_window(
|
||||
"elemente_properties",
|
||||
title="Element — Eigenschaften",
|
||||
size=(480, 720),
|
||||
bridge=b)
|
||||
@@ -0,0 +1,303 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
elemente_uebersicht.py
|
||||
BIM-artiger Project Browser: alle Smart-Elemente in einem Tree
|
||||
gruppiert nach Geschoss → Kind → Element. Eigene Satellite-Window
|
||||
(Eto.Form + WebView), liest seine Daten direkt aus dem ActiveDoc
|
||||
via elemente._read_meta. Klick auf eine Zeile selektiert das Objekt
|
||||
in Rhino.
|
||||
"""
|
||||
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 elemente as _elm
|
||||
|
||||
|
||||
_KIND_MAP = {
|
||||
"wand_axis": "wand",
|
||||
"decke_outline": "decke",
|
||||
"dach_outline": "dach",
|
||||
"treppe_axis": "treppe",
|
||||
"stuetze_point": "stuetze",
|
||||
"traeger_axis": "traeger",
|
||||
"raum_outline": "raum",
|
||||
"stempel": "stempel",
|
||||
"decke_aussparung_outline": "aussparung",
|
||||
"oeffnung_point": "oeffnung", # wird zu fenster/tuer aufgeloest
|
||||
}
|
||||
|
||||
|
||||
def _safe_float(v, default=None):
|
||||
try: return float(v)
|
||||
except Exception: return default
|
||||
|
||||
|
||||
def _build_overview(doc):
|
||||
"""Sammelt alle Smart-Element-Sources, gruppiert nach Geschoss +
|
||||
Kind. Returns dict mit 'geschosse' (geordnete Liste) + 'items'
|
||||
(Flat-Liste pro Geschoss/Kind). Frontend baut den Tree."""
|
||||
if doc is None:
|
||||
return {"geschosse": [], "items": []}
|
||||
geschosse = _elm._load_geschosse(doc) or []
|
||||
items = []
|
||||
seen = set()
|
||||
for obj in doc.Objects:
|
||||
meta = _elm._read_meta(obj)
|
||||
if meta is None: continue
|
||||
t = meta.get("type")
|
||||
if t not in _elm.SOURCE_TYPES: continue
|
||||
if meta["id"] in seen: continue
|
||||
seen.add(meta["id"])
|
||||
|
||||
kind = _KIND_MAP.get(t, t)
|
||||
if t == "oeffnung_point":
|
||||
kind = meta.get("oeff_typ", "fenster")
|
||||
|
||||
g = _elm._geschoss_by_id(doc, meta.get("geschoss"))
|
||||
g_id = (g.get("id") if g else "") or "__keingeschoss__"
|
||||
g_name = g.get("name") if g else "(kein Geschoss)"
|
||||
|
||||
# Kompakte Property-Zusammenfassung pro Element-Typ
|
||||
info = ""
|
||||
try:
|
||||
if kind == "wand":
|
||||
info = "d {:.2f} m".format(meta.get("dicke", 0) or 0)
|
||||
elif kind == "decke":
|
||||
info = "d {:.2f} m".format(meta.get("dicke", 0) or 0)
|
||||
elif kind == "dach":
|
||||
info = "d {:.2f} m · {:.0f}°".format(
|
||||
meta.get("dicke", 0) or 0, meta.get("neigung", 0) or 0)
|
||||
elif kind in ("fenster", "tuer"):
|
||||
info = "{:.2f}×{:.2f} m".format(
|
||||
meta.get("oeff_breite", 0) or 0,
|
||||
meta.get("oeff_hoehe", 0) or 0)
|
||||
elif kind == "treppe":
|
||||
info = "{} St".format(meta.get("treppe_n_stufen", "?"))
|
||||
elif kind in ("stuetze", "traeger"):
|
||||
profil = meta.get("trag_profil", "?")
|
||||
info = "{}".format(profil)
|
||||
elif kind == "raum":
|
||||
info = meta.get("raum_name", "") or "Raum"
|
||||
elif kind == "aussparung":
|
||||
info = "Aussparung"
|
||||
except Exception: pass
|
||||
|
||||
items.append({
|
||||
"id": meta["id"],
|
||||
"objectId": str(obj.Id),
|
||||
"kind": kind,
|
||||
"geschossId": g_id,
|
||||
"geschossName": g_name,
|
||||
"name": meta.get("raum_name") or "",
|
||||
"info": info,
|
||||
"selected": obj.IsSelected(False) > 0,
|
||||
})
|
||||
|
||||
# Geschoss-Liste (geordnet wie in doc.Strings)
|
||||
out_geschosse = []
|
||||
for g in geschosse:
|
||||
if not isinstance(g, dict): continue
|
||||
out_geschosse.append({
|
||||
"id": g.get("id") or "",
|
||||
"name": g.get("name") or "?",
|
||||
"okff": _safe_float(g.get("okff"), 0.0),
|
||||
})
|
||||
# "(kein Geschoss)" anhaengen wenn es Elemente ohne Geschoss gibt
|
||||
if any(it["geschossId"] == "__keingeschoss__" for it in items):
|
||||
out_geschosse.append({
|
||||
"id": "__keingeschoss__", "name": "(kein Geschoss)", "okff": None,
|
||||
})
|
||||
|
||||
# SIA-416 Bilanz pro Geschoss: aggregiert alle raum_outline-Flaechen
|
||||
# nach raum_sia-Klassifikation. Räume ohne SIA-Tag landen in "ohne".
|
||||
# NF = HNF + NNF (Nutzflaeche). Wird im Frontend als Tabelle gerendert.
|
||||
sia_bilanz = {} # {geschossId: {hnf, nnf, vf, ff, ohne, nf, total, count}}
|
||||
for obj in doc.Objects:
|
||||
meta = _elm._read_meta(obj)
|
||||
if meta is None: continue
|
||||
if meta.get("type") != "raum_outline": continue
|
||||
try:
|
||||
area, _, _ = _elm._raum_amp(obj.Geometry)
|
||||
except Exception: continue
|
||||
if not area or area <= 0: continue
|
||||
g_id = meta.get("geschoss") or "__keingeschoss__"
|
||||
sia = (meta.get("raum_sia") or "").lower()
|
||||
if sia not in ("hnf", "nnf", "vf", "ff", "gf", "agf"):
|
||||
sia = "ohne"
|
||||
b = sia_bilanz.setdefault(g_id, {
|
||||
"hnf": 0.0, "nnf": 0.0, "vf": 0.0, "ff": 0.0,
|
||||
"gf": 0.0, "agf": 0.0,
|
||||
"ohne": 0.0, "count": 0,
|
||||
})
|
||||
b[sia] += float(area)
|
||||
b["count"] += 1
|
||||
# NF/NGF/Total ableiten
|
||||
for b in sia_bilanz.values():
|
||||
b["nf"] = b["hnf"] + b["nnf"]
|
||||
b["ngf"] = b["nf"] + b["vf"] + b["ff"]
|
||||
b["total"] = (b["hnf"] + b["nnf"] + b["vf"] + b["ff"]
|
||||
+ b["gf"] + b["agf"] + b["ohne"])
|
||||
|
||||
return {"geschosse": out_geschosse, "items": items,
|
||||
"siaBilanz": sia_bilanz}
|
||||
|
||||
|
||||
class ElementeUebersichtBridge(panel_base.BaseBridge):
|
||||
def __init__(self):
|
||||
panel_base.BaseBridge.__init__(self, "elemente_uebersicht")
|
||||
|
||||
def _send_state(self):
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
self.send("STATE", _build_overview(doc))
|
||||
|
||||
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 = {}
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
|
||||
if t == "READY" or t == "REQUEST_STATE":
|
||||
self._on_ready()
|
||||
elif t == "SELECT_ELEMENT":
|
||||
obj_id_str = p.get("objectId") or ""
|
||||
try:
|
||||
import System
|
||||
guid = System.Guid(obj_id_str)
|
||||
obj = doc.Objects.FindId(guid)
|
||||
if obj is not None:
|
||||
doc.Objects.UnselectAll()
|
||||
obj.Select(True)
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[UEBERSICHT] select:", ex)
|
||||
self._send_state()
|
||||
elif t == "ZOOM_TO_ELEMENT":
|
||||
obj_id_str = p.get("objectId") or ""
|
||||
try:
|
||||
import System
|
||||
guid = System.Guid(obj_id_str)
|
||||
obj = doc.Objects.FindId(guid)
|
||||
if obj is not None:
|
||||
doc.Objects.UnselectAll()
|
||||
obj.Select(True)
|
||||
try:
|
||||
vp = doc.Views.ActiveView.ActiveViewport
|
||||
bb = obj.Geometry.GetBoundingBox(True)
|
||||
if bb.IsValid:
|
||||
bb.Inflate(bb.Diagonal.Length * 0.5,
|
||||
bb.Diagonal.Length * 0.5,
|
||||
bb.Diagonal.Length * 0.5)
|
||||
vp.ZoomBoundingBox(bb)
|
||||
doc.Views.Redraw()
|
||||
except Exception as ex:
|
||||
print("[UEBERSICHT] zoom:", ex)
|
||||
except Exception as ex:
|
||||
print("[UEBERSICHT] zoom find:", ex)
|
||||
elif t == "EXPORT_BILANZ":
|
||||
self._export_bilanz()
|
||||
|
||||
def _export_bilanz(self):
|
||||
"""Exportiert SIA-416 Bilanz als CSV (Excel-kompatibel: Semikolon-
|
||||
Separator + UTF-8 BOM + Komma als Dezimaltrenner). Wide-Format:
|
||||
eine Spalte pro Geschoss + Total-Spalte, Zeilen pro Kategorie.
|
||||
"""
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
# Geschoss-Liste (geordnet) + Total am Ende
|
||||
geschosse = _elm._load_geschosse(doc) or []
|
||||
gs_list = [g for g in geschosse
|
||||
if isinstance(g, dict) and g.get("isGeschoss")]
|
||||
# Bilanz pro Geschoss + Total via compute_sia_bilanz
|
||||
per_gid = {} # gid → bilanz dict
|
||||
for g in gs_list:
|
||||
per_gid[g["id"]] = _elm.compute_sia_bilanz(
|
||||
doc, "geschoss:" + g["id"])
|
||||
total = _elm.compute_sia_bilanz(doc, "total")
|
||||
# SaveFileDialog
|
||||
try:
|
||||
from Rhino.UI import SaveFileDialog
|
||||
sfd = SaveFileDialog()
|
||||
sfd.DefaultExt = "csv"
|
||||
sfd.Filter = "CSV (*.csv)|*.csv"
|
||||
sfd.FileName = "sia_bilanz.csv"
|
||||
ok = False
|
||||
try: ok = sfd.ShowSaveDialog()
|
||||
except Exception:
|
||||
try: ok = sfd.ShowDialog()
|
||||
except Exception: ok = False
|
||||
if not ok:
|
||||
print("[UEBERSICHT] Bilanz-Export abgebrochen"); return
|
||||
path = sfd.FileName
|
||||
except Exception as ex:
|
||||
print("[UEBERSICHT] SaveFileDialog:", ex); return
|
||||
|
||||
# Zeilen-Definition: (Label, Bilanz-Key, ist_personen?)
|
||||
rows = [
|
||||
("HNF (m²)", "hnf", False),
|
||||
("NNF (m²)", "nnf", False),
|
||||
("NF (m²)", "nf", False),
|
||||
("VF (m²)", "vf", False),
|
||||
("FF (m²)", "ff", False),
|
||||
("NGF (m²)", "ngf", False),
|
||||
("GF (m²)", "gf", False),
|
||||
("AGF (m²)", "agf", False),
|
||||
("Räume", "count", True),
|
||||
("Personen", "personen", True),
|
||||
]
|
||||
|
||||
def _fmt(val, is_count):
|
||||
if val is None: return ""
|
||||
if is_count: return str(int(val))
|
||||
return "{:.2f}".format(float(val)).replace(".", ",")
|
||||
|
||||
def _esc(s):
|
||||
s = str(s)
|
||||
if ";" in s or '"' in s or "\n" in s:
|
||||
return '"' + s.replace('"', '""') + '"'
|
||||
return s
|
||||
|
||||
try:
|
||||
import io
|
||||
with io.open(path, "w", encoding="utf-8-sig", newline="") as f:
|
||||
# Header — Kategorie + Geschoss-Namen + Total
|
||||
header = ["Kategorie"]
|
||||
for g in gs_list: header.append(_esc(g.get("name") or "?"))
|
||||
header.append("Total")
|
||||
f.write(";".join(header) + "\n")
|
||||
for label, key, is_count in rows:
|
||||
line = [_esc(label)]
|
||||
for g in gs_list:
|
||||
b = per_gid.get(g["id"], {})
|
||||
line.append(_fmt(b.get(key, 0), is_count))
|
||||
line.append(_fmt(total.get(key, 0), is_count))
|
||||
f.write(";".join(line) + "\n")
|
||||
print("[UEBERSICHT] SIA-Bilanz exportiert: {} ({} Geschosse + Total)".format(
|
||||
path, len(gs_list)))
|
||||
except Exception as ex:
|
||||
print("[UEBERSICHT] CSV schreiben:", ex)
|
||||
|
||||
|
||||
def open_as_window():
|
||||
"""Oeffnet die Element-Uebersicht als Satellite-Window."""
|
||||
b = ElementeUebersichtBridge()
|
||||
sc.sticky["elemente_uebersicht_bridge"] = b
|
||||
panel_base.open_satellite_window(
|
||||
"elemente_uebersicht",
|
||||
title="Elemente — Übersicht",
|
||||
size=(540, 720),
|
||||
bridge=b)
|
||||
@@ -1,11 +1,13 @@
|
||||
#! python 3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
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"
|
||||
_-RunPythonScript "/Users/karim/STUDIO/DOSSIER/rhino/inspect_section.py"
|
||||
"""
|
||||
import Rhino
|
||||
|
||||
@@ -54,7 +56,7 @@ for n in dir(layer):
|
||||
except Exception as ex:
|
||||
print(" layer.{} -> err: {}".format(n, ex))
|
||||
|
||||
# layer.SectionStyle dumpen wenn vorhanden
|
||||
# layer.SectionStyle dumpen wenn present
|
||||
try:
|
||||
if hasattr(layer, "SectionStyle"):
|
||||
dump("layer.SectionStyle", layer.SectionStyle)
|
||||
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
kamera.py
|
||||
Kamera-Panel: liest/setzt Viewport-Kamera (Position, Target, Projektion,
|
||||
FOV/Frustum, Linse). Persistiert Presets in doc.Strings unter
|
||||
`dossier_kamera_presets` (JSON-Liste).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
import uuid
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
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 = "9c3f8a2d-7b4e-4a6f-9c12-1d4e5f6a7b89"
|
||||
_PRESETS_KEY = "dossier_kamera_presets"
|
||||
_NORTH_KEY = "dossier_north_angle" # Grad im Uhrzeigersinn von +Y
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Norden-Rotation: Default +Y = Norden. Bei rotierten Projekten (Site-Plaene,
|
||||
# swissBUILDINGS in LV95-Orientierung) kann der User einen Winkel definieren.
|
||||
# Wirkt auf N/O/S/W-View-Buttons + (optional) Iso-Octanten.
|
||||
|
||||
def get_north_angle(doc):
|
||||
if doc is None: return 0.0
|
||||
try:
|
||||
v = doc.Strings.GetValue(_NORTH_KEY)
|
||||
return float(v) if v else 0.0
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
def set_north_angle(doc, angle):
|
||||
if doc is None: return
|
||||
try:
|
||||
a = float(angle) % 360.0
|
||||
doc.Strings.SetString(_NORTH_KEY, "{:.3f}".format(a))
|
||||
except Exception as ex:
|
||||
print("[KAMERA] set north:", ex)
|
||||
|
||||
|
||||
def _scene_target_and_diag(doc):
|
||||
"""Centroid der Szenen-BBox + Diagonal-Laenge. Fallback (0,0,0)/50m."""
|
||||
target = rg.Point3d(0, 0, 0)
|
||||
diag = 50.0
|
||||
try:
|
||||
scene_bb = None
|
||||
for obj in doc.Objects:
|
||||
if obj is None: continue
|
||||
gb = obj.Geometry.GetBoundingBox(True)
|
||||
if not gb.IsValid: continue
|
||||
if scene_bb is None: scene_bb = gb
|
||||
else: scene_bb.Union(gb)
|
||||
if scene_bb is not None and scene_bb.IsValid:
|
||||
target = scene_bb.Center
|
||||
diag = max(scene_bb.Diagonal.Length, 10.0)
|
||||
except Exception: pass
|
||||
return target, diag
|
||||
|
||||
|
||||
def set_cardinal_view(vp, cardinal):
|
||||
"""Setzt N/O/S/W-Ansicht unter Beruecksichtigung der Norden-Rotation.
|
||||
cardinal: 'N' | 'O' | 'S' | 'W'."""
|
||||
if vp is None: return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
try:
|
||||
north_deg = get_north_angle(doc)
|
||||
# Norden-Einheitsvektor (Uhrzeigersinn-Rotation von +Y aus Top-View):
|
||||
# angle=0 → (0,1,0); angle=90 → (1,0,0); angle=180 → (0,-1,0).
|
||||
nrad = math.radians(north_deg)
|
||||
north = (math.sin(nrad), math.cos(nrad))
|
||||
east = (math.cos(nrad), -math.sin(nrad))
|
||||
c = cardinal.upper()
|
||||
if c == "N": dx, dy = north
|
||||
elif c == "S": dx, dy = -north[0], -north[1]
|
||||
elif c == "O" or c == "E": dx, dy = east
|
||||
elif c == "W": dx, dy = -east[0], -east[1]
|
||||
else: return
|
||||
|
||||
target, diag = _scene_target_and_diag(doc)
|
||||
dist = diag * 1.6
|
||||
# Kamera auf Hoehe des Target-Z (echte Elevation, horizontal blickend)
|
||||
loc = rg.Point3d(target.X + dx * dist,
|
||||
target.Y + dy * dist,
|
||||
target.Z)
|
||||
if not vp.IsParallelProjection:
|
||||
vp.ChangeToParallelProjection(True)
|
||||
vp.SetCameraLocations(target, loc)
|
||||
# Camera-Up muss +Z sein (sonst kippt die Ansicht)
|
||||
try: vp.CameraUp = rg.Vector3d.ZAxis
|
||||
except Exception: pass
|
||||
try: vp.ZoomExtents()
|
||||
except Exception: pass
|
||||
Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw()
|
||||
except Exception as ex:
|
||||
print("[KAMERA] set cardinal:", ex)
|
||||
|
||||
|
||||
def set_top_view(vp):
|
||||
"""Plan-Ansicht (oben), rotiert nach Norden-Setting."""
|
||||
if vp is None: return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
try:
|
||||
target, diag = _scene_target_and_diag(doc)
|
||||
loc = rg.Point3d(target.X, target.Y, target.Z + diag * 2)
|
||||
if not vp.IsParallelProjection:
|
||||
vp.ChangeToParallelProjection(True)
|
||||
vp.SetCameraLocations(target, loc)
|
||||
# Plan-Norden zeigt nach oben im Viewport → Up-Vector = north
|
||||
north_deg = get_north_angle(doc)
|
||||
nrad = math.radians(north_deg)
|
||||
try:
|
||||
vp.CameraUp = rg.Vector3d(math.sin(nrad), math.cos(nrad), 0)
|
||||
except Exception: pass
|
||||
try: vp.ZoomExtents()
|
||||
except Exception: pass
|
||||
Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw()
|
||||
except Exception as ex:
|
||||
print("[KAMERA] set top:", ex)
|
||||
|
||||
|
||||
def _read_viewport(vp):
|
||||
"""Liest den aktuellen Zustand eines Viewports als dict."""
|
||||
if vp is None: return None
|
||||
try:
|
||||
loc = vp.CameraLocation
|
||||
tgt = vp.CameraTarget
|
||||
is_par = bool(vp.IsParallelProjection)
|
||||
# FOV in Grad (vertical) — gilt nur fuer Perspective
|
||||
try:
|
||||
fov_v = float(vp.Camera35mmLensLength) # Linsen-Brennweite mm
|
||||
except Exception:
|
||||
fov_v = 50.0
|
||||
# Frustum-Breite (m bei m-Doc) — gilt nur fuer Parallel
|
||||
try:
|
||||
f = vp.GetFrustum()
|
||||
# GetFrustum returns (bool ok, l, r, b, t, n, f)
|
||||
ok = bool(f[0]) if isinstance(f, tuple) else False
|
||||
if ok:
|
||||
_l = f[1]; _r = f[2]
|
||||
frustum_w = abs(_r - _l)
|
||||
else:
|
||||
frustum_w = 0.0
|
||||
except Exception:
|
||||
frustum_w = 0.0
|
||||
# Distanz Kamera→Target
|
||||
dx = loc.X - tgt.X; dy = loc.Y - tgt.Y; dz = loc.Z - tgt.Z
|
||||
dist = math.sqrt(dx*dx + dy*dy + dz*dz)
|
||||
return {
|
||||
"name": vp.Name or "",
|
||||
"parallel": is_par,
|
||||
"loc": [loc.X, loc.Y, loc.Z],
|
||||
"target": [tgt.X, tgt.Y, tgt.Z],
|
||||
"lensMm": fov_v,
|
||||
"frustumW": frustum_w,
|
||||
"distance": dist,
|
||||
}
|
||||
except Exception as ex:
|
||||
print("[KAMERA] read viewport:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def _active_viewport():
|
||||
try:
|
||||
v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView
|
||||
return v.ActiveViewport if v else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _set_viewport(vp, data):
|
||||
"""Schreibt Position/Target/Projektion/Linse zurueck."""
|
||||
if vp is None or not isinstance(data, dict): return
|
||||
try:
|
||||
# Projektion zuerst — danach Frustum/Linse konsistent
|
||||
if "parallel" in data:
|
||||
par = bool(data.get("parallel"))
|
||||
if par != bool(vp.IsParallelProjection):
|
||||
if par: vp.ChangeToParallelProjection(True)
|
||||
else: vp.ChangeToPerspectiveProjection(True, 50.0)
|
||||
loc_cur = vp.CameraLocation
|
||||
tgt_cur = vp.CameraTarget
|
||||
loc_arr = data.get("loc")
|
||||
tgt_arr = data.get("target")
|
||||
if isinstance(loc_arr, list) and len(loc_arr) == 3:
|
||||
loc_cur = rg.Point3d(float(loc_arr[0]), float(loc_arr[1]), float(loc_arr[2]))
|
||||
if isinstance(tgt_arr, list) and len(tgt_arr) == 3:
|
||||
tgt_cur = rg.Point3d(float(tgt_arr[0]), float(tgt_arr[1]), float(tgt_arr[2]))
|
||||
vp.SetCameraLocations(tgt_cur, loc_cur)
|
||||
# Linse / Frustum
|
||||
if not vp.IsParallelProjection and "lensMm" in data:
|
||||
try:
|
||||
lens = float(data.get("lensMm") or 50.0)
|
||||
vp.Camera35mmLensLength = lens
|
||||
except Exception: pass
|
||||
if vp.IsParallelProjection and "frustumW" in data:
|
||||
try:
|
||||
w = float(data.get("frustumW") or 0.0)
|
||||
if w > 0:
|
||||
# Rhino hat keine direkte SetFrustumWidth — wir nutzen
|
||||
# SetViewProjection-Hack via SetFrustum (l, r, b, t, n, f)
|
||||
cur = vp.GetFrustum()
|
||||
if isinstance(cur, tuple) and len(cur) >= 7 and cur[0]:
|
||||
l_cur, r_cur, b_cur, t_cur, n_cur, f_cur = cur[1], cur[2], cur[3], cur[4], cur[5], cur[6]
|
||||
cur_w = abs(r_cur - l_cur)
|
||||
if cur_w > 1e-9:
|
||||
ratio = w / cur_w
|
||||
mid_lr = (l_cur + r_cur) / 2.0
|
||||
mid_bt = (b_cur + t_cur) / 2.0
|
||||
half_w = w / 2.0
|
||||
half_h = abs(t_cur - b_cur) / 2.0 * ratio
|
||||
vp.SetFrustum(mid_lr - half_w, mid_lr + half_w,
|
||||
mid_bt - half_h, mid_bt + half_h,
|
||||
n_cur, f_cur)
|
||||
except Exception as ex:
|
||||
print("[KAMERA] frustum set:", ex)
|
||||
try:
|
||||
Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw()
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[KAMERA] set viewport:", ex)
|
||||
|
||||
|
||||
def _set_iso(vp, octant="NE"):
|
||||
"""Setzt eine standard-architektonische Iso-Ansicht (35.26° vertikal,
|
||||
45° horizontal). octant: 'NE' | 'NW' | 'SE' | 'SW' — die XY-Diagonale
|
||||
relativ zum Norden des Projekts (= rotiert mit dossier_north_angle)."""
|
||||
if vp is None: return
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
try:
|
||||
target, diag = _scene_target_and_diag(doc)
|
||||
# Octant-Basisvektor (vor Norden-Rotation): NE = (+1,+1)
|
||||
sign_e = 1 if "E" in octant.upper() else -1
|
||||
sign_n = 1 if "N" in octant.upper() else -1
|
||||
north_deg = get_north_angle(doc)
|
||||
nrad = math.radians(north_deg)
|
||||
# Norden-Vektor + Osten-Vektor im Welt-Koordinatensystem
|
||||
north_vec = (math.sin(nrad), math.cos(nrad))
|
||||
east_vec = (math.cos(nrad), -math.sin(nrad))
|
||||
# Iso-XY-Richtung = sign_e * east + sign_n * north
|
||||
dx = sign_e * east_vec[0] + sign_n * north_vec[0]
|
||||
dy = sign_e * east_vec[1] + sign_n * north_vec[1]
|
||||
dz = 1.0
|
||||
L = math.sqrt(dx*dx + dy*dy + dz*dz)
|
||||
dist = diag * 1.6
|
||||
loc = rg.Point3d(target.X + dx/L * dist,
|
||||
target.Y + dy/L * dist,
|
||||
target.Z + dz/L * dist)
|
||||
if not vp.IsParallelProjection:
|
||||
vp.ChangeToParallelProjection(True)
|
||||
vp.SetCameraLocations(target, loc)
|
||||
try: vp.ZoomExtents()
|
||||
except Exception: pass
|
||||
Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw()
|
||||
except Exception as ex:
|
||||
print("[KAMERA] set iso:", ex)
|
||||
|
||||
|
||||
def _load_presets(doc):
|
||||
try:
|
||||
raw = doc.Strings.GetValue(_PRESETS_KEY)
|
||||
if not raw: return []
|
||||
items = json.loads(raw)
|
||||
return items if isinstance(items, list) else []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _save_presets(doc, items):
|
||||
try:
|
||||
doc.Strings.SetString(_PRESETS_KEY, json.dumps(items))
|
||||
except Exception as ex:
|
||||
print("[KAMERA] save presets:", ex)
|
||||
|
||||
|
||||
def _payload():
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
vp = _active_viewport()
|
||||
return {
|
||||
"viewport": _read_viewport(vp),
|
||||
"presets": _load_presets(doc),
|
||||
"northAngle": get_north_angle(doc),
|
||||
}
|
||||
|
||||
|
||||
class KameraBridge(panel_base.BaseBridge):
|
||||
def __init__(self):
|
||||
panel_base.BaseBridge.__init__(self, "kamera")
|
||||
|
||||
def _on_ready(self):
|
||||
self._send_state()
|
||||
|
||||
def _send_state(self):
|
||||
self.send("STATE", _payload())
|
||||
|
||||
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_VIEWPORT":
|
||||
vp = _active_viewport()
|
||||
_set_viewport(vp, p)
|
||||
self._send_state()
|
||||
elif t == "SET_ISO":
|
||||
vp = _active_viewport()
|
||||
_set_iso(vp, p.get("octant") or "NE")
|
||||
self._send_state()
|
||||
elif t == "SET_NORTH_ANGLE":
|
||||
set_north_angle(Rhino.RhinoDoc.ActiveDoc, p.get("angle") or 0)
|
||||
self._send_state()
|
||||
# Topbar refreshen damit dort der neue Winkel sichtbar ist
|
||||
try:
|
||||
b = sc.sticky.get("oberleiste_bridge")
|
||||
if b is not None: b._send_state(force=True)
|
||||
except Exception: pass
|
||||
elif t == "SET_PROJECTION":
|
||||
vp = _active_viewport()
|
||||
if vp is not None:
|
||||
par = bool(p.get("parallel"))
|
||||
if par: vp.ChangeToParallelProjection(True)
|
||||
else: vp.ChangeToPerspectiveProjection(True, 50.0)
|
||||
try: Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw()
|
||||
except Exception: pass
|
||||
self._send_state()
|
||||
elif t == "ZOOM_EXTENTS":
|
||||
vp = _active_viewport()
|
||||
if vp is not None:
|
||||
try: vp.ZoomExtents()
|
||||
except Exception: pass
|
||||
try: Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.Redraw()
|
||||
except Exception: pass
|
||||
self._send_state()
|
||||
elif t == "SAVE_PRESET":
|
||||
name = (p.get("name") or "").strip() or "Preset"
|
||||
vp = _active_viewport()
|
||||
state = _read_viewport(vp)
|
||||
if state is None: return
|
||||
items = _load_presets(doc)
|
||||
entry = {
|
||||
"id": "kp_" + uuid.uuid4().hex[:8],
|
||||
"name": name,
|
||||
"parallel": state["parallel"],
|
||||
"loc": state["loc"],
|
||||
"target": state["target"],
|
||||
"lensMm": state["lensMm"],
|
||||
"frustumW": state["frustumW"],
|
||||
}
|
||||
items.append(entry)
|
||||
_save_presets(doc, items)
|
||||
self._send_state()
|
||||
elif t == "APPLY_PRESET":
|
||||
pid = p.get("id")
|
||||
items = _load_presets(doc)
|
||||
entry = next((x for x in items if x.get("id") == pid), None)
|
||||
if entry is None: return
|
||||
vp = _active_viewport()
|
||||
_set_viewport(vp, entry)
|
||||
self._send_state()
|
||||
elif t == "DELETE_PRESET":
|
||||
pid = p.get("id")
|
||||
items = [x for x in _load_presets(doc) if x.get("id") != pid]
|
||||
_save_presets(doc, items)
|
||||
self._send_state()
|
||||
|
||||
|
||||
def open_as_window():
|
||||
"""Oeffnet KAMERA als Eto-Form + WebView (analog Overrides)."""
|
||||
b = KameraBridge()
|
||||
sc.sticky["kamera_bridge"] = b
|
||||
panel_base.open_satellite_window(
|
||||
"kamera",
|
||||
title="Kamera",
|
||||
size=(420, 600),
|
||||
bridge=b)
|
||||
+491
-110
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
layer_builder.py
|
||||
Layer-Struktur:
|
||||
@@ -68,19 +70,307 @@ def _add_layer(doc, name, parent_id=None, color=None, lw=None):
|
||||
return doc.Layers.Add(layer)
|
||||
|
||||
|
||||
def _find_hatch_pattern_index(doc, name):
|
||||
"""Sucht einen Hatch-Pattern-Index per Name (case-insensitive). -1 wenn nicht da."""
|
||||
if not name or name == "None":
|
||||
return -1
|
||||
target = name.strip().lower()
|
||||
try:
|
||||
for i in range(doc.HatchPatterns.Count):
|
||||
hp = doc.HatchPatterns[i]
|
||||
if hp is None or hp.IsDeleted: continue
|
||||
if hp.Name and hp.Name.strip().lower() == target:
|
||||
return i
|
||||
except Exception as ex:
|
||||
print("[LAYERS] hatch lookup:", ex)
|
||||
return -1
|
||||
|
||||
|
||||
def _find_linetype_index(doc, name):
|
||||
"""Sucht einen Linetype-Index per Name. -1 = ByLayer."""
|
||||
if not name or name in ("byLayer", "by_layer", "ByLayer"):
|
||||
return -1
|
||||
target = name.strip().lower()
|
||||
try:
|
||||
for i in range(doc.Linetypes.Count):
|
||||
lt = doc.Linetypes[i]
|
||||
if lt is None or lt.IsDeleted: continue
|
||||
if lt.Name and lt.Name.strip().lower() == target:
|
||||
return i
|
||||
except Exception: pass
|
||||
return -1
|
||||
|
||||
|
||||
def _try_set(obj, prop_names, value):
|
||||
"""Versucht den Wert auf das erste presente Property zu setzen.
|
||||
Liefert den Property-Namen bei Erfolg, sonst None."""
|
||||
if isinstance(prop_names, str):
|
||||
prop_names = (prop_names,)
|
||||
for prop in prop_names:
|
||||
if hasattr(obj, prop):
|
||||
try:
|
||||
setattr(obj, prop, value)
|
||||
return prop
|
||||
except Exception as ex:
|
||||
# Property da, aber Wert nicht akzeptiert (z.B. enum-conversion)
|
||||
# — anderen Namen probieren statt aufgeben
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _enum_int(*candidates):
|
||||
"""Liefert den ersten Enum-Wert aus den Kandidaten der existiert.
|
||||
candidates = list of (module-path-list, value-name). Bei keiner Match
|
||||
liefert None."""
|
||||
for path, val_name in candidates:
|
||||
try:
|
||||
obj = Rhino
|
||||
for p in path:
|
||||
obj = getattr(obj, p)
|
||||
return getattr(obj, val_name)
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _apply_section_style(doc, layer, section_cfg, layer_color):
|
||||
"""Setzt einen Custom-SectionStyle auf den Layer aus dem Dossier-section-dict.
|
||||
|
||||
Nutzt Rhino-8's Python-3-API (Rhino.DocObjects.SectionStyle +
|
||||
Layer.SetCustomSectionStyle / RemoveCustomSectionStyle). In IPy 2.7
|
||||
sind diese Methoden nicht exponiert — dort no-op.
|
||||
|
||||
Wichtig: viele Farb-/Linetype-Properties greifen nur wenn der
|
||||
zugehoerige "*Source"-Wert auf ColorFromObject / LinetypeFromObject
|
||||
steht. Das setzen wir explizit.
|
||||
"""
|
||||
if not section_cfg or not isinstance(section_cfg, dict):
|
||||
return
|
||||
has_setter = hasattr(layer, "SetCustomSectionStyle")
|
||||
has_remover = hasattr(layer, "RemoveCustomSectionStyle")
|
||||
if not has_setter:
|
||||
return # IPy-2.7 — keine API
|
||||
|
||||
try:
|
||||
SS = Rhino.DocObjects.SectionStyle
|
||||
except Exception as ex:
|
||||
print("[LAYERS] SectionStyle-Klasse nicht da:", ex); return
|
||||
|
||||
pat = (section_cfg.get("hatchPattern") or "None").strip()
|
||||
show = bool(section_cfg.get("boundaryShow", True))
|
||||
diag = "[SS:{}]".format(layer.Name if layer else "?")
|
||||
# DEBUG: zeigt was an section_cfg ankommt (zur Diagnose des Hatch-Bugs)
|
||||
print(diag, "section_cfg.hatchPattern='{}' scale={} rot={}".format(
|
||||
pat, section_cfg.get("hatchScale"), section_cfg.get("hatchRotation")))
|
||||
|
||||
# Wenn weder Hatch noch Boundary → Custom-Style entfernen
|
||||
if pat == "None" and not show:
|
||||
if has_remover:
|
||||
try:
|
||||
layer.RemoveCustomSectionStyle()
|
||||
print(diag, "removed (kein Hatch + kein Boundary)")
|
||||
except Exception as ex:
|
||||
print(diag, "remove failed:", ex)
|
||||
return
|
||||
|
||||
style = SS()
|
||||
|
||||
# Property-Inventar einmal beim ersten Aufruf loggen — hilft bei
|
||||
# API-Verschiebungen zwischen Rhino-Versionen sofort den Mismatch zu sehen.
|
||||
if not getattr(_apply_section_style, "_props_logged", False):
|
||||
props = [n for n in dir(style)
|
||||
if not n.startswith("_") and not callable(getattr(style, n, None))]
|
||||
print("[SS] verfuegbare Properties:", ", ".join(sorted(props)))
|
||||
_apply_section_style._props_logged = True
|
||||
|
||||
# --- Hatch ---
|
||||
if pat and pat != "None":
|
||||
hp_idx = _find_hatch_pattern_index(doc, pat)
|
||||
if hp_idx >= 0:
|
||||
set_to = _try_set(style, ("HatchIndex", "HatchPatternIndex"), hp_idx)
|
||||
print(diag, "HatchIndex={} via {}".format(hp_idx, set_to))
|
||||
else:
|
||||
print(diag, "Hatch '{}' nicht in HatchPatterns gefunden".format(pat))
|
||||
|
||||
scale_v = float(section_cfg.get("hatchScale") or 1.0)
|
||||
_try_set(style, ("HatchScale", "HatchPatternScale"), scale_v)
|
||||
|
||||
import math
|
||||
rot_deg = float(section_cfg.get("hatchRotation") or 0)
|
||||
_try_set(style, ("HatchRotation", "HatchAngle"), math.radians(rot_deg))
|
||||
|
||||
# Hatch-Color: explizit setzen — wenn User keine Override-Farbe angegeben
|
||||
# hat, nehmen wir die Layer-Farbe als Default (sonst rendert Rhino sonst
|
||||
# schwarz). Section-Style hat keine ByLayer-Option, also Farbwert
|
||||
# explizit reinkopieren.
|
||||
hatch_color = section_cfg.get("hatchColor")
|
||||
if hatch_color:
|
||||
col = _color(hatch_color)
|
||||
elif layer_color is not None:
|
||||
col = _color(layer_color) if isinstance(layer_color, str) else layer_color
|
||||
else:
|
||||
col = None
|
||||
if col is not None:
|
||||
set_color = _try_set(style, ("HatchPatternColor", "HatchColor", "FillColor"), col)
|
||||
print(diag, "HatchColor via {} (default=layer)".format(set_color))
|
||||
|
||||
# Background (viewport=0/transparent vs object=1)
|
||||
bg = section_cfg.get("background")
|
||||
if bg in ("object", "byObject"):
|
||||
# Versuche Enum-Konstanten zu finden
|
||||
for prop_names, en_paths in (
|
||||
(("BackgroundFillMode", "BackgroundColorUsage", "FillBackground"),
|
||||
(("DocObjects", "SectionBackgroundFillMode"), "SolidColor")),
|
||||
):
|
||||
en_val = _enum_int(en_paths)
|
||||
if en_val is not None and _try_set(style, prop_names, en_val):
|
||||
break
|
||||
# Fallback: bool/int = 1
|
||||
else:
|
||||
_try_set(style, ("FillBackground",), True)
|
||||
|
||||
# --- Boundary ---
|
||||
set_show = _try_set(style, ("BoundaryVisible", "ShowBoundary"), show)
|
||||
print(diag, "BoundaryVisible={} via {}".format(show, set_show))
|
||||
|
||||
if show:
|
||||
# Boundary-Color: User-Override oder Layer-Farbe als Default
|
||||
bc = section_cfg.get("boundaryColor")
|
||||
if bc:
|
||||
bcol = _color(bc)
|
||||
elif layer_color is not None:
|
||||
bcol = _color(layer_color) if isinstance(layer_color, str) else layer_color
|
||||
else:
|
||||
bcol = None
|
||||
if bcol is not None:
|
||||
set_to = _try_set(style,
|
||||
("BoundaryColor", "OutlineColor", "EdgeColor"), bcol)
|
||||
print(diag, "BoundaryColor via {} (default=layer)".format(set_to))
|
||||
|
||||
# Width-Scale auf PlotWeight uebertragen (RW8 hat keine WidthScale direkt;
|
||||
# alternative Property-Namen probieren)
|
||||
ws = float(section_cfg.get("boundaryWidthScale") or 1.0)
|
||||
set_to = _try_set(style,
|
||||
("BoundaryWidthScale", "EdgeWidthScale", "OutlineWidthScale",
|
||||
"PlotWeightScale"), ws)
|
||||
if not set_to:
|
||||
# Direkte PlotWeight setzen wenn Layer-PlotWeight bekannt
|
||||
try:
|
||||
base_lw = float(getattr(layer, "PlotWeight", 0.25) or 0.25)
|
||||
except Exception:
|
||||
base_lw = 0.25
|
||||
_try_set(style, ("BoundaryPlotWeight", "PlotWeight"), base_lw * ws)
|
||||
|
||||
# Linetype: Index + Source auf LinetypeFromObject
|
||||
lt = section_cfg.get("boundaryLinetype")
|
||||
if lt and lt not in ("byLayer", "ByLayer"):
|
||||
lt_idx = _find_linetype_index(doc, lt)
|
||||
set_to = _try_set(style,
|
||||
("BoundaryLinetypeIndex", "EdgeLinetypeIndex"), lt_idx)
|
||||
lt_src = _enum_int(
|
||||
(("DocObjects", "ObjectLinetypeSource"), "LinetypeFromObject"))
|
||||
if lt_src is not None:
|
||||
_try_set(style,
|
||||
("BoundaryLinetypeSource", "EdgeLinetypeSource"),
|
||||
lt_src)
|
||||
print(diag, "BoundaryLinetype={} idx={} via {}".format(lt, lt_idx, set_to))
|
||||
|
||||
# SectionOpenObjects: bei nicht-geschlossener Geometrie auch schneiden
|
||||
soo = bool(section_cfg.get("sectionOpenObjects", True))
|
||||
_try_set(style, ("SectionOpenObjects", "ClipOpenObjects",
|
||||
"SectionCutsOpenObjects"), soo)
|
||||
|
||||
# Style auf Layer setzen + explizit Modify damit Mac-Rhino den Layer
|
||||
# persistiert (sonst greift's nicht immer)
|
||||
try:
|
||||
layer.SetCustomSectionStyle(style)
|
||||
except Exception as ex:
|
||||
print(diag, "SetCustomSectionStyle FAIL:", ex)
|
||||
return
|
||||
try:
|
||||
doc.Layers.Modify(layer, layer.LayerIndex, True)
|
||||
except Exception: pass
|
||||
print(diag, "OK applied")
|
||||
|
||||
|
||||
def walk_ebenen(ebenen, parent_path=()):
|
||||
"""Iteriert Ebenen-Baum (flach + Children). Liefert Tuples
|
||||
(path, ebene) wobei path ein Tuple der Codes von der Root bis zu dieser
|
||||
Ebene ist (inkl. eigener Code). Beispiel:
|
||||
walk_ebenen([{'code':'20','children':[{'code':'01'}]}])
|
||||
→ [(('20',), e20), (('20','01'), e01)]"""
|
||||
out = []
|
||||
if not ebenen: return out
|
||||
for e in ebenen:
|
||||
if not isinstance(e, dict): continue
|
||||
code = e.get("code")
|
||||
if not code: continue
|
||||
path = parent_path + (code,)
|
||||
out.append((path, e))
|
||||
children = e.get("children")
|
||||
if isinstance(children, list) and children:
|
||||
out.extend(walk_ebenen(children, path))
|
||||
return out
|
||||
|
||||
|
||||
def _build_ebene_layer(doc, parent_id, e, diag_prefix=""):
|
||||
"""Findet/erstellt einen Sublayer fuer eine Ebene unter parent_id.
|
||||
Liefert den layer_idx oder -1. Setzt Farbe/LW/Section-Style."""
|
||||
code = e.get("code") or ""
|
||||
name = e.get("name") or "Ebene"
|
||||
sub_name = "{}_{}".format(code, name) if code else name
|
||||
col = _color(e.get("color"))
|
||||
lw = float(e.get("lw", 0.13))
|
||||
sub_idx = _find_sublayer_by_code(doc, parent_id, code) if code else -1
|
||||
if sub_idx < 0:
|
||||
sub_idx = _add_layer(doc, sub_name, parent_id, col, lw)
|
||||
if sub_idx >= 0 and code:
|
||||
doc.Layers[sub_idx].SetUserString("dossier_code", 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
|
||||
if code: sub.SetUserString("dossier_code", code)
|
||||
# Section Style anwenden (Py3-only — IPy 2.7 no-op)
|
||||
try:
|
||||
_apply_section_style(doc, doc.Layers[sub_idx],
|
||||
e.get("section"), e.get("color"))
|
||||
except Exception as ex:
|
||||
print("[LAYERS] section-style apply ({}{}): {}".format(
|
||||
diag_prefix, sub_name, ex))
|
||||
return sub_idx
|
||||
|
||||
|
||||
def _build_ebenen_recursive(doc, parent_id, ebenen, diag_prefix=""):
|
||||
"""Rekursive Ebenen-Erstellung: jeder Eintrag wird als Sublayer angelegt,
|
||||
seine 'children' werden unter dem neu erstellten Sublayer angelegt."""
|
||||
if not ebenen: return
|
||||
for e in ebenen:
|
||||
if not isinstance(e, dict): continue
|
||||
sub_idx = _build_ebene_layer(doc, parent_id, e, diag_prefix=diag_prefix)
|
||||
if sub_idx < 0: continue
|
||||
children = e.get("children")
|
||||
if isinstance(children, list) and children:
|
||||
child_parent_id = doc.Layers[sub_idx].Id
|
||||
_build_ebenen_recursive(doc, child_parent_id, children,
|
||||
diag_prefix=diag_prefix + e.get("name", "") + "/")
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
"""Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert
|
||||
und unter jedem alle Ebenen (rekursiv inkl. children) 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 = _find_top_by_name(doc, z_name)
|
||||
if idx < 0:
|
||||
idx = _add_layer(doc, z_name)
|
||||
doc.Layers[idx].SetUserString("dossier_id", z_id)
|
||||
@@ -89,48 +379,34 @@ def build_layers(doc, zeichnungsebenen, ebenen):
|
||||
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"])
|
||||
|
||||
_build_ebenen_recursive(doc, parent_id, ebenen,
|
||||
diag_prefix=z_name + "/")
|
||||
doc.Views.Redraw()
|
||||
print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert".format(
|
||||
len(zeichnungsebenen), len(ebenen)))
|
||||
n_total = len(walk_ebenen(ebenen))
|
||||
print("[LAYERS] {} drawing levels x {} layers updated (incl. {} sub)".format(
|
||||
len(zeichnungsebenen), len(ebenen), max(0, n_total - len(ebenen))))
|
||||
|
||||
|
||||
def _layer_matches_code(layer, code):
|
||||
"""True wenn der Layer zu der Ebene mit `code` gehoert. Akzeptiert
|
||||
sowohl Top-Sub-Layer (Geschoss/CODE_Name) als auch Sub-Sub-Layer
|
||||
(Geschoss/Parent/CODE_Name) — Match via Name-Prefix `code_`."""
|
||||
if _is_top_level(layer): return False
|
||||
return layer.Name.startswith(code + "_")
|
||||
|
||||
|
||||
def update_layer_style(doc, code, color_hex=None, lw=None):
|
||||
"""Aendert Farbe und/oder Stiftdicke fuer alle Sublayer mit dem gegebenen Code."""
|
||||
"""Aendert Farbe und/oder Stiftdicke fuer alle Sublayer mit dem gegebenen
|
||||
Code — auch tief verschachtelte (Sub-Sub-Layer mit gleichem Code-Prefix)."""
|
||||
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
|
||||
for layer in doc.Layers:
|
||||
if not _layer_matches_code(layer, code): continue
|
||||
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))
|
||||
@@ -140,20 +416,16 @@ def update_layer_style(doc, code, color_hex=None, lw=None):
|
||||
|
||||
|
||||
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 + "_"):
|
||||
"""Schaltet alle Sublayer mit Code in/aus (auch tief verschachtelte)."""
|
||||
for layer in doc.Layers:
|
||||
if _layer_matches_code(layer, 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 + "_"):
|
||||
for layer in doc.Layers:
|
||||
if _layer_matches_code(layer, code):
|
||||
layer.IsLocked = locked
|
||||
doc.Views.Redraw()
|
||||
|
||||
@@ -161,7 +433,7 @@ def set_ebene_locked(doc, code, locked):
|
||||
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
|
||||
Falls move_to set: verschiebt vorher alle Objekte zum Sublayer
|
||||
mit move_to-Code unter dem selben Parent. Sonst: loescht Objekte mit.
|
||||
"""
|
||||
if not code:
|
||||
@@ -215,10 +487,10 @@ def delete_ebene(doc, code, move_to=None):
|
||||
if doc.Layers.Delete(from_idx, True):
|
||||
deleted_layers += 1
|
||||
except Exception as ex:
|
||||
print("[EBENEN] Layer-Delete:", ex)
|
||||
print("[LAYERS] Layer-Delete:", ex)
|
||||
|
||||
doc.Views.Redraw()
|
||||
print("[EBENEN] Ebene {} entfernt: {} Sublayer, {} Objekte verschoben, {} Objekte geloescht".format(
|
||||
print("[LAYERS] Ebene {} entfernt: {} Sublayer, {} Objekte verschoben, {} Objekte geloescht".format(
|
||||
code, deleted_layers, moved, deleted_objs))
|
||||
|
||||
|
||||
@@ -241,48 +513,125 @@ 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.
|
||||
|
||||
Diagnostik via [CLIP]-Praefix in den Print-Ausgaben — bei Problemen die
|
||||
Rhino-Konsole nach 'CLIP' filtern.
|
||||
"""
|
||||
import Rhino.Geometry as rg
|
||||
z_name = (active_z or {}).get("name") if active_z else None
|
||||
print("[CLIP] update: enabled={} active='{}' isGeschoss={} okff={} sh={}".format(
|
||||
enabled,
|
||||
z_name,
|
||||
bool(active_z and active_z.get("isGeschoss")),
|
||||
(active_z or {}).get("okff"),
|
||||
(active_z or {}).get("schnitthoehe"),
|
||||
))
|
||||
|
||||
existing = _find_clipping_plane(doc)
|
||||
print("[CLIP] existing: {}".format(existing.Id if existing else "none"))
|
||||
|
||||
is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None)
|
||||
if (not enabled) or (not is_geschoss):
|
||||
|
||||
# IMMER presente Plane loeschen — bei Re-Enable wollen wir frische
|
||||
# vp_ids (alte koennten leer/falsch sein, dann clippt das Replace zwar
|
||||
# die Geometrie aber keinen Viewport).
|
||||
if existing is not None:
|
||||
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)
|
||||
print("[CLIP] alte Plane geloescht")
|
||||
except Exception as ex:
|
||||
print("[EBENEN] Clip-Update:", ex)
|
||||
else:
|
||||
print("[CLIP] Delete failed:", ex)
|
||||
|
||||
if (not enabled) or (not is_geschoss):
|
||||
print("[CLIP] disabled — done (enabled={}, isGeschoss={})".format(enabled, is_geschoss))
|
||||
doc.Views.Redraw()
|
||||
return
|
||||
|
||||
# dict.get(k, default) liefert default NUR wenn Key fehlt — bei
|
||||
# Key-present-aber-None gibt's None zurueck. float(None) crasht.
|
||||
# Daher explizit None-faangen:
|
||||
okff_raw = active_z.get("okff")
|
||||
sh_raw = active_z.get("schnitthoehe")
|
||||
okff = float(okff_raw) if okff_raw is not None else 0.0
|
||||
sh = float(sh_raw) if sh_raw is not None else 1.0
|
||||
cut_z = okff + sh
|
||||
print("[CLIP] cut_z={} (okff={}, schnitthoehe={})".format(cut_z, okff, sh))
|
||||
# Normal nach -Z = sichtbar bleibt UNTERHALB der Plane (Grundriss-Schnitt:
|
||||
# man steht auf der Boden-Seite, alles darueber wird weggeschnitten).
|
||||
# Mit +Z waere es genau umgekehrt (Decke + Rest oben sichtbar).
|
||||
plane = rg.Plane(rg.Point3d(0.0, 0.0, cut_z), rg.Vector3d(0.0, 0.0, -1.0))
|
||||
du, dv = 50000.0, 50000.0
|
||||
|
||||
# Viewport-IDs sammeln — wir wollen ALLE Modell-Viewports clippen,
|
||||
# nicht nur den gerade aktiven. Sammeln aus mehreren Quellen +
|
||||
# dedupen damit die Plane in Top/Front/Right/Perspective gleichzeitig
|
||||
# wirkt.
|
||||
vp_ids = []
|
||||
for view in doc.Views:
|
||||
seen = set()
|
||||
def _add(vpid):
|
||||
if vpid is None: return
|
||||
try:
|
||||
vp_ids.append(view.ActiveViewportID)
|
||||
except Exception:
|
||||
try: vp_ids.append(view.ActiveViewport.Id)
|
||||
key = str(vpid)
|
||||
except Exception: return
|
||||
if key in seen or vpid == System.Guid.Empty: return
|
||||
seen.add(key); vp_ids.append(vpid)
|
||||
|
||||
# Methode 1: GetViewList — alle Modell-Views (kein Page-Layout)
|
||||
try:
|
||||
views = doc.Views.GetViewList(True, False)
|
||||
for v in views:
|
||||
try: _add(v.ActiveViewport.Id)
|
||||
except Exception: pass
|
||||
try: _add(v.MainViewport.Id)
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[CLIP] GetViewList Fehler:", ex)
|
||||
|
||||
# Methode 2: Iteration ueber Views (Fallback falls GetViewList anders)
|
||||
try:
|
||||
for view in doc.Views:
|
||||
try: _add(view.ActiveViewport.Id)
|
||||
except Exception: pass
|
||||
try: _add(view.MainViewport.Id)
|
||||
except Exception: pass
|
||||
try: _add(view.ActiveViewportID)
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[CLIP] doc.Views iteration Fehler:", ex)
|
||||
|
||||
# Namen fuer Debug-Output sammeln
|
||||
vp_names = []
|
||||
try:
|
||||
for view in doc.Views:
|
||||
try: vp_names.append(view.ActiveViewport.Name)
|
||||
except Exception: pass
|
||||
except Exception: pass
|
||||
|
||||
print("[CLIP] {} Viewport-ID(s) gesammelt: {}".format(
|
||||
len(vp_ids), ", ".join(vp_names) or "(keine Namen)"))
|
||||
if not vp_ids:
|
||||
print("[CLIP] WARNUNG: keine Viewports — Plane wuerde nichts schneiden")
|
||||
|
||||
try:
|
||||
new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
|
||||
if new_id != System.Guid.Empty:
|
||||
if new_id == System.Guid.Empty:
|
||||
print("[CLIP] AddClippingPlane lieferte Empty Guid — Fehler")
|
||||
return
|
||||
obj = doc.Objects.FindId(new_id)
|
||||
if obj is not None:
|
||||
if obj is None:
|
||||
print("[CLIP] FindId nach Erstellung lieferte None — Object weg")
|
||||
return
|
||||
attrs = obj.Attributes.Duplicate()
|
||||
attrs.SetUserString(_CLIP_KEY, "1")
|
||||
attrs.Mode = Rhino.DocObjects.ObjectMode.Locked
|
||||
# Mode = Normal damit die Plane in Mac Rhino voll sichtbar ist.
|
||||
# Locked-Mode rendert auf Mac oft nur ein blasses Edge. Wer den
|
||||
# Plane-Boundary nicht selektieren will, kann via Layer locken.
|
||||
attrs.Mode = Rhino.DocObjects.ObjectMode.Normal
|
||||
doc.Objects.ModifyAttributes(obj, attrs, True)
|
||||
print("[EBENEN] Clipping-Plane bei Z={} erstellt".format(cut_z))
|
||||
print("[CLIP] Plane erstellt: Z={}, ID={}, du/dv={}/{}".format(
|
||||
cut_z, new_id, du, dv))
|
||||
except Exception as ex:
|
||||
print("[EBENEN] Clip-Create:", ex)
|
||||
print("[CLIP] AddClippingPlane Fehler:", ex)
|
||||
doc.Views.Redraw()
|
||||
|
||||
|
||||
@@ -307,31 +656,56 @@ def cleanup_default_layers(doc):
|
||||
except Exception:
|
||||
pass
|
||||
if deleted:
|
||||
print("[EBENEN] Default-Layer entfernt: {}".format(", ".join(deleted)))
|
||||
print("[LAYERS] Default layer removed: {}".format(", ".join(deleted)))
|
||||
|
||||
|
||||
def _find_sublayer_by_code_recursive(doc, parent_id, code):
|
||||
"""Sucht einen Sub-Layer mit `code` unter parent_id — auch tief
|
||||
verschachtelt (Sub-Sub-Layer mit gleichem Code-Prefix). Liefert
|
||||
layer_index oder -1."""
|
||||
prefix = code + "_"
|
||||
direct = []
|
||||
for i, layer in enumerate(doc.Layers):
|
||||
if layer is None or layer.IsDeleted: continue
|
||||
if layer.ParentLayerId == parent_id:
|
||||
if layer.Name.startswith(prefix): return i
|
||||
direct.append(layer.Id)
|
||||
for child_id in direct:
|
||||
idx = _find_sublayer_by_code_recursive(doc, child_id, code)
|
||||
if idx >= 0: return idx
|
||||
return -1
|
||||
|
||||
|
||||
def set_active_sublayer(doc, zeichnungsebene_id, code):
|
||||
"""Macht den Sublayer 'code' unter Zeichnungsebene 'zeichnungsebene_id' aktiv."""
|
||||
"""Macht den Sublayer 'code' unter Zeichnungsebene 'zeichnungsebene_id'
|
||||
aktiv. Sucht rekursiv durch verschachtelte Sub-Layer (z.B. 70_osm/
|
||||
7101_Strassen liegt zwei Ebenen tief)."""
|
||||
parent_idx = _find_top_by_id(doc, zeichnungsebene_id)
|
||||
if parent_idx < 0:
|
||||
print("[EBENEN] Parent-Layer fuer Zeichnungsebene {} nicht gefunden".format(zeichnungsebene_id))
|
||||
print("[LAYERS] Parent-Layer fuer Zeichnungsebene {} not found".format(zeichnungsebene_id))
|
||||
return
|
||||
parent_id = doc.Layers[parent_idx].Id
|
||||
sub_idx = _find_sublayer_by_code(doc, parent_id, code)
|
||||
sub_idx = _find_sublayer_by_code_recursive(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))
|
||||
print("[LAYERS] Sublayer with code {} under parent {} not found".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'
|
||||
|
||||
Versteht den hierarchischen Ebenen-Baum: Children erben ParentLayerId vom
|
||||
Sub-Layer (nicht vom Geschoss). Sub-Sub-Layer werden rekursiv mitgepflegt.
|
||||
"""
|
||||
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}
|
||||
# Flat walk durch Ebenen-Tree (top + children) — alle Codes mit ihren
|
||||
# Eye/Lock-Flags.
|
||||
flat_ebenen = [e for _path, e in walk_ebenen(ebenen)]
|
||||
canonical = {e["code"]: _color(e.get("color")) for e in flat_ebenen}
|
||||
e_eye_vis = {e["code"]: e.get("visible", True) for e in flat_ebenen}
|
||||
e_eye_locked = {e["code"]: e.get("locked", False) for e in flat_ebenen}
|
||||
|
||||
id_to_top, name_to_top, children_by_parent = {}, {}, {}
|
||||
for layer in doc.Layers:
|
||||
@@ -350,12 +724,18 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
|
||||
children = children_by_parent.get(parent.Id, [])
|
||||
is_active_z = z["id"] == active_z_id
|
||||
z_visible_flag = z.get("visible", True)
|
||||
z_locked_flag = bool(z.get("locked", False))
|
||||
|
||||
# Z-Mode -> Parent-Zustand
|
||||
# 'all_force' kommt VOR dem visible-Flag-Check: zeigt jede Z auch wenn
|
||||
# das User-Eye sie ausgeblendet hatte. 'all' dagegen respektiert das
|
||||
# Eye-Flag (= "Ausgewählte" im UI).
|
||||
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 z_mode == "all_force":
|
||||
p_vis, p_grey, p_lock = True, False, False
|
||||
elif not z_visible_flag:
|
||||
p_vis, p_grey, p_lock = False, False, False
|
||||
elif z_mode == "all":
|
||||
@@ -365,6 +745,11 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
|
||||
else: # grey
|
||||
p_vis, p_grey, p_lock = True, True, False
|
||||
|
||||
# Per-Z explizites Sperren ueberlagert (auch fuer die aktive Z) — wer
|
||||
# eine Geschoss-Ebene sperrt, will dass Klicks ins Leere gehen.
|
||||
if z_locked_flag:
|
||||
p_lock = True
|
||||
|
||||
parent_changed = False
|
||||
if parent.IsVisible != p_vis:
|
||||
parent.IsVisible = p_vis
|
||||
@@ -379,21 +764,24 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
|
||||
if not p_vis:
|
||||
continue # Children erben Parent-Hidden
|
||||
|
||||
# E-Mode -> Sublayer-Zustand
|
||||
for child in children:
|
||||
if "_" not in child.Name:
|
||||
continue
|
||||
# E-Mode → Sub-Layer (rekursiv durch Tree; Sub-Sub-Layer haben Parent
|
||||
# = Sub-Layer, nicht das Geschoss — also iterativ in die Tiefe).
|
||||
def _apply_to_sublayer(child, p_grey_eff):
|
||||
if "_" not in child.Name: return
|
||||
code = child.Name.split("_", 1)[0]
|
||||
if code not in canonical:
|
||||
continue
|
||||
if code not in canonical: return
|
||||
is_active_e = (code == active_code)
|
||||
eye_v = e_eye_vis.get(code, True)
|
||||
eye_l = e_eye_locked.get(code, False)
|
||||
|
||||
# 'all_force' ueberschreibt das Eye-Flag (zeigt jede Ebene auch
|
||||
# wenn die User-Sichtbarkeit aus war). 'all' respektiert das
|
||||
# Flag (= "Ausgewählte" im UI).
|
||||
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 e_mode == "all_force":
|
||||
e_vis, e_grey, e_lock = True, False, False
|
||||
elif not eye_v:
|
||||
e_vis, e_grey, e_lock = False, False, False
|
||||
elif e_mode == "all":
|
||||
@@ -402,35 +790,28 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_
|
||||
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_grey = p_grey_eff or e_grey
|
||||
child_lock = e_lock or eye_l
|
||||
|
||||
changed = False
|
||||
if child.IsVisible != child_vis:
|
||||
child.IsVisible = child_vis
|
||||
changed = True
|
||||
child.IsVisible = child_vis; changed = True
|
||||
if child.IsLocked != child_lock:
|
||||
child.IsLocked = child_lock
|
||||
changed = True
|
||||
child.IsLocked = child_lock; changed = True
|
||||
if child_grey:
|
||||
if child.Color != GREY:
|
||||
child.Color = GREY
|
||||
changed = True
|
||||
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:
|
||||
child.Color = canon; changed = True
|
||||
if changed:
|
||||
try:
|
||||
doc.Layers.Modify(child, child.LayerIndex, True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try: doc.Layers.Modify(child, child.LayerIndex, True)
|
||||
except Exception: pass
|
||||
# Sub-Sub-Layer rekursiv (Children dieses Sub-Layers).
|
||||
# Sub-Sub-Layer erben den 'grey'-Zustand des Parents.
|
||||
for grand in children_by_parent.get(child.Id, []):
|
||||
_apply_to_sublayer(grand, child_grey)
|
||||
for child in children:
|
||||
_apply_to_sublayer(child, p_grey)
|
||||
doc.Views.Redraw()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+75
-8
@@ -1,9 +1,11 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
layouts.py
|
||||
LAYOUTS-Panel: Layout-Pages erstellen + Details mit Ausschnitten bestuecken.
|
||||
Phase 1 — Snapshot-Mode: Ausschnitt wird beim Zuweisen auf das Detail angewendet,
|
||||
Phase 1 — Snapshot-Mode: Ausschnitt wird beim Zuweisen auf das Detail applied,
|
||||
Re-Sync per Knopf. Live-Link und Masterlayouts kommen spaeter.
|
||||
"""
|
||||
import os
|
||||
@@ -281,6 +283,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
||||
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 "")
|
||||
elif t == "OPEN_LAYOUT_DIALOG": self._open_layout_dialog(p)
|
||||
|
||||
# --- State-Snapshot -----------------------------------------------------
|
||||
|
||||
@@ -386,7 +389,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
||||
try:
|
||||
page = doc.Views.AddPageView(name, w, h)
|
||||
if page is None:
|
||||
print("[LAYOUTS] AddPageView fehlgeschlagen"); return
|
||||
print("[LAYOUTS] AddPageView failed"); return
|
||||
print("[LAYOUTS] '{}' angelegt ({}x{})".format(name, w, h))
|
||||
except Exception as ex:
|
||||
print("[LAYOUTS] AddPageView Fehler:", ex)
|
||||
@@ -411,7 +414,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
||||
done = True
|
||||
print("[LAYOUTS] SetPageSize -> {}x{}".format(w, h))
|
||||
except Exception as ex:
|
||||
print("[LAYOUTS] SetPageSize fehlgeschlagen:", ex)
|
||||
print("[LAYOUTS] SetPageSize failed:", ex)
|
||||
# 2) Fallback: Properties (haengt von Rhino-Version ab)
|
||||
if not done:
|
||||
try:
|
||||
@@ -420,7 +423,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
||||
done = True
|
||||
print("[LAYOUTS] PageWidth/Height-Properties -> {}x{}".format(w, h))
|
||||
except Exception as ex:
|
||||
print("[LAYOUTS] Property-Setter fehlgeschlagen:", ex)
|
||||
print("[LAYOUTS] Property-Setter failed:", ex)
|
||||
if not done:
|
||||
print("[LAYOUTS] Konnte Seiten-Groesse nicht setzen — bitte ueber Rhinos Layout-Dialog aendern")
|
||||
try: page.Redraw()
|
||||
@@ -512,7 +515,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
||||
pdf.Write(path)
|
||||
print("[LAYOUTS] PDF geschrieben: {} ({} Seite(n))".format(path, n_added))
|
||||
except Exception as ex:
|
||||
print("[LAYOUTS] PDF-Export fehlgeschlagen:", ex)
|
||||
print("[LAYOUTS] PDF-Export failed:", ex)
|
||||
finally:
|
||||
# Vorherige View wieder aktivieren
|
||||
if prev_view is not None:
|
||||
@@ -535,7 +538,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
||||
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
|
||||
# Default-Folder — neben der .3dm wenn present
|
||||
if doc.Path:
|
||||
try: dlg.Directory = System.Uri(os.path.dirname(doc.Path))
|
||||
except Exception: pass
|
||||
@@ -737,6 +740,69 @@ class LayoutsBridge(panel_base.BaseBridge):
|
||||
print("[LAYOUTS] sync layout:", ex)
|
||||
self._send_state()
|
||||
|
||||
def _open_layout_dialog(self, p):
|
||||
"""Oeffnet ein Satelliten-Fenster mit dem Layout-Erstellen/Bearbeiten
|
||||
Dialog. mode = 'new' | 'edit'. Bei 'edit' wird `layout` (id, name,
|
||||
width, height) mitgeschickt."""
|
||||
outer = self
|
||||
mode = (p.get("mode") or "new")
|
||||
layout = p.get("layout") or None
|
||||
bridge_holder = {"form": None}
|
||||
|
||||
def _apply(payload):
|
||||
if mode == "new":
|
||||
outer._new_layout({
|
||||
"name": payload.get("name") or "",
|
||||
"format": payload.get("format") or "A3",
|
||||
"landscape": bool(payload.get("landscape", True)),
|
||||
"customWidth": payload.get("customWidth"),
|
||||
"customHeight": payload.get("customHeight"),
|
||||
})
|
||||
elif mode == "edit" and layout and layout.get("id"):
|
||||
outer._set_page_size({
|
||||
"id": layout.get("id"),
|
||||
"format": payload.get("format") or "A3",
|
||||
"landscape": bool(payload.get("landscape", True)),
|
||||
"customWidth": payload.get("customWidth"),
|
||||
"customHeight": payload.get("customHeight"),
|
||||
})
|
||||
|
||||
class _LayoutDialogBridge(panel_base.BaseBridge):
|
||||
def __init__(self):
|
||||
panel_base.BaseBridge.__init__(self, "layout_dialog")
|
||||
def _on_ready(self):
|
||||
self.send("LAYOUT_DIALOG_STATE", {
|
||||
"mode": mode,
|
||||
"layout": layout,
|
||||
})
|
||||
def handle(self, data):
|
||||
if not isinstance(data, dict): return
|
||||
t = data.get("type", "")
|
||||
pp = data.get("payload") or {}
|
||||
if t == "READY":
|
||||
self._on_ready()
|
||||
elif t == "SAVE":
|
||||
_apply(pp)
|
||||
try:
|
||||
f = bridge_holder.get("form")
|
||||
if f is not None: f.Close()
|
||||
except Exception: pass
|
||||
elif t == "CANCEL":
|
||||
try:
|
||||
f = bridge_holder.get("form")
|
||||
if f is not None: f.Close()
|
||||
except Exception: pass
|
||||
|
||||
b = _LayoutDialogBridge()
|
||||
title = "Neues Layout" if mode == "new" else "Papierformat: {}".format(
|
||||
(layout or {}).get("name", ""))
|
||||
bridge_holder["form"] = panel_base.open_satellite_window(
|
||||
"layout_dialog",
|
||||
params={"mode": mode, "layout": layout},
|
||||
title=title,
|
||||
size=(440, 380),
|
||||
bridge=b)
|
||||
|
||||
|
||||
def _bridge_factory():
|
||||
b = LayoutsBridge()
|
||||
@@ -744,5 +810,6 @@ def _bridge_factory():
|
||||
return b
|
||||
|
||||
|
||||
panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR,
|
||||
_bridge_factory, icon_spec=("L", "#7a5fa8"))
|
||||
panel_base.register_and_open("layouts", "Layouts", PANEL_GUID_STR,
|
||||
_bridge_factory,
|
||||
icon_spec=("view_quilt", "#7a5fa8"))
|
||||
|
||||
@@ -0,0 +1,853 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
library.py — Dossier-Library (Phase A: lokal, read-only)
|
||||
|
||||
Library = wiederverwendbare Material-/Symbol-/Object-Templates die ein User
|
||||
oder ein Team teilt. Phase A: lokaler Ordner mit library.json + Assets.
|
||||
Spaeter (Phase B/C): Cloud-Sync via GitHub-Releases.
|
||||
|
||||
Library-Root: ~/Library/Application Support/Dossier/library/
|
||||
Struktur:
|
||||
library/
|
||||
library.json # Manifest (Schema-Version + items[])
|
||||
previews/ # PNG-Thumbnails
|
||||
assets/ # .3dm-Fragmente fuer symbol/object
|
||||
|
||||
Manifest-Item:
|
||||
{
|
||||
"id": "mat-beton-roh-v1", # global eindeutig (UUID/Slug)
|
||||
"type": "material", # material | symbol | object
|
||||
"name": "Beton roh",
|
||||
"version": 1, # Schema-Version des Items
|
||||
"tags": ["beton", "tragwerk"],
|
||||
"preview": "previews/mat-beton-roh.png",
|
||||
"data": { ... } # Typ-spezifische Felder
|
||||
}
|
||||
|
||||
Material-data: { color, hatch, scale }
|
||||
Symbol/Object-data: { files: ["assets/sym-foo.3dm"] }
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
|
||||
# Lazy Import von Rhino — library.py soll auch in einem Test-Skript ohne
|
||||
# Rhino-Kontext importierbar sein (z.B. fuer Seed-Logik).
|
||||
try:
|
||||
import Rhino # noqa: F401
|
||||
except Exception:
|
||||
Rhino = None
|
||||
|
||||
|
||||
_LIB_DIRNAME = "library"
|
||||
_MANIFEST_FN = "library.json"
|
||||
_SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
def library_root():
|
||||
"""Mac-Pfad zur lokalen Library. Wird bei Bedarf angelegt."""
|
||||
base = os.path.expanduser(
|
||||
"~/Library/Application Support/Dossier")
|
||||
return os.path.join(base, _LIB_DIRNAME)
|
||||
|
||||
|
||||
def ensure_library():
|
||||
"""Legt Library-Folder + Sub-Folders + Seed-Manifest an wenn leer."""
|
||||
root = library_root()
|
||||
if not os.path.isdir(root):
|
||||
try: os.makedirs(root)
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] mkdir:", ex); return root
|
||||
for sub in ("previews", "assets"):
|
||||
p = os.path.join(root, sub)
|
||||
if not os.path.isdir(p):
|
||||
try: os.makedirs(p)
|
||||
except Exception: pass
|
||||
manifest_path = os.path.join(root, _MANIFEST_FN)
|
||||
if not os.path.isfile(manifest_path):
|
||||
_write_seed_manifest(manifest_path)
|
||||
return root
|
||||
|
||||
|
||||
def _write_seed_manifest(path):
|
||||
"""Bootstrappt 4 Materialien + 2 Symbol/Object-Beispiel-Eintraege als
|
||||
Start. Die Symbol/Object-Files muss der User selber ablegen unter
|
||||
library/assets/ — sonst schlaegt der Import fehl (graceful)."""
|
||||
seed = {
|
||||
"schemaVersion": _SCHEMA_VERSION,
|
||||
"name": "Dossier-Library (lokal)",
|
||||
"items": [
|
||||
{
|
||||
"id": "mat-beton-sichtbeton-v1",
|
||||
"type": "material", "version": 1,
|
||||
"name": "Beton — Sichtbeton",
|
||||
"tags": ["beton", "tragwerk", "roh"],
|
||||
"data": {"color": "#a8a39b"},
|
||||
},
|
||||
{
|
||||
"id": "mat-mauerwerk-backstein-v1",
|
||||
"type": "material", "version": 1,
|
||||
"name": "Mauerwerk — Backstein",
|
||||
"tags": ["mauerwerk", "stein"],
|
||||
"data": {"color": "#a45a3c"},
|
||||
},
|
||||
{
|
||||
"id": "mat-daemmung-mineralwolle-v1",
|
||||
"type": "material", "version": 1,
|
||||
"name": "Daemmung — Mineralwolle",
|
||||
"tags": ["daemmung", "weich"],
|
||||
"data": {"color": "#e8d36b"},
|
||||
},
|
||||
{
|
||||
"id": "mat-holz-fichte-v1",
|
||||
"type": "material", "version": 1,
|
||||
"name": "Holz — Fichte",
|
||||
"tags": ["holz", "ausbau"],
|
||||
"data": {"color": "#c8a06a"},
|
||||
},
|
||||
{
|
||||
"id": "sym-nordpfeil-01",
|
||||
"type": "symbol", "version": 1,
|
||||
"name": "Nordpfeil",
|
||||
"tags": ["plan", "nordpfeil"],
|
||||
"files": ["assets/sym-nordpfeil-01.3dm"],
|
||||
},
|
||||
{
|
||||
"id": "obj-baum-laubbaum-01",
|
||||
"type": "object", "version": 1,
|
||||
"name": "Baum — Laubbaum",
|
||||
"tags": ["aussen", "vegetation"],
|
||||
"files": ["assets/obj-baum-laubbaum-01.3dm"],
|
||||
},
|
||||
],
|
||||
}
|
||||
try:
|
||||
with open(path, "w") as f:
|
||||
json.dump(seed, f, indent=2, ensure_ascii=False)
|
||||
print("[LIBRARY] Seed geschrieben:", path)
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] seed write:", ex)
|
||||
|
||||
|
||||
def load_manifest():
|
||||
"""Liest library.json — legt Library bei Bedarf an. Returns dict mit
|
||||
schemaVersion + name + items[]. Bei Fehler leeres Manifest."""
|
||||
ensure_library()
|
||||
path = os.path.join(library_root(), _MANIFEST_FN)
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict): return _empty_manifest()
|
||||
items = data.get("items")
|
||||
if not isinstance(items, list): data["items"] = []
|
||||
else: data["items"] = [_normalize_item(x) for x in items
|
||||
if _normalize_item(x) is not None]
|
||||
return data
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] load_manifest:", ex)
|
||||
return _empty_manifest()
|
||||
|
||||
|
||||
def save_manifest(manifest):
|
||||
"""Schreibt das Manifest zurueck zur library.json. Items werden
|
||||
normalisiert. Returns True/False."""
|
||||
ensure_library()
|
||||
path = os.path.join(library_root(), _MANIFEST_FN)
|
||||
try:
|
||||
if not isinstance(manifest, dict): manifest = _empty_manifest()
|
||||
manifest.setdefault("schemaVersion", _SCHEMA_VERSION)
|
||||
manifest.setdefault("name", "Dossier-Library")
|
||||
items = manifest.get("items") or []
|
||||
manifest["items"] = [_normalize_item(x) for x in items
|
||||
if _normalize_item(x) is not None]
|
||||
with open(path, "w") as f:
|
||||
json.dump(manifest, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] save_manifest:", ex)
|
||||
return False
|
||||
|
||||
|
||||
def update_item(item_id, patch):
|
||||
"""Patcht ein Item im Manifest. Returns (ok, new_manifest)."""
|
||||
m = load_manifest()
|
||||
items = m.get("items", [])
|
||||
found = False
|
||||
for it in items:
|
||||
if it.get("id") == item_id:
|
||||
for k, v in (patch or {}).items():
|
||||
it[k] = v
|
||||
found = True
|
||||
break
|
||||
if not found: return False, m
|
||||
ok = save_manifest(m)
|
||||
return ok, load_manifest()
|
||||
|
||||
|
||||
def delete_item(item_id):
|
||||
"""Loescht ein Item aus dem Manifest. Asset-Files bleiben auf Disk
|
||||
(User koennte sie noch wollen)."""
|
||||
m = load_manifest()
|
||||
items = m.get("items", [])
|
||||
new_items = [it for it in items if it.get("id") != item_id]
|
||||
if len(new_items) == len(items): return False, m
|
||||
m["items"] = new_items
|
||||
ok = save_manifest(m)
|
||||
return ok, load_manifest()
|
||||
|
||||
|
||||
def add_item(item):
|
||||
"""Fuegt ein neues Item zum Manifest hinzu. Returns (ok, new_manifest)."""
|
||||
norm = _normalize_item(item)
|
||||
if norm is None: return False, load_manifest()
|
||||
m = load_manifest()
|
||||
items = m.get("items", [])
|
||||
# Dedupe per id
|
||||
items = [it for it in items if it.get("id") != norm["id"]]
|
||||
items.append(norm)
|
||||
m["items"] = items
|
||||
ok = save_manifest(m)
|
||||
return ok, load_manifest()
|
||||
|
||||
|
||||
def _previews_dir():
|
||||
"""Pfad zum previews/-Subfolder. Wird angelegt falls fehlt."""
|
||||
p = os.path.join(library_root(), "previews")
|
||||
if not os.path.isdir(p):
|
||||
try: os.makedirs(p)
|
||||
except Exception: pass
|
||||
return p
|
||||
|
||||
|
||||
def _preview_rel_for(asset_rel_or_id):
|
||||
"""Erzeugt einen relativen Preview-Pfad fuer ein Asset (oder Item-ID).
|
||||
Liefert z.B. 'previews/<name>.png'. Wir benutzen den Stamm des
|
||||
.3dm-Files damit Asset + Preview gleichen Namen haben (debuggbar)."""
|
||||
stem = asset_rel_or_id
|
||||
if "/" in stem: stem = stem.split("/")[-1]
|
||||
if "\\" in stem: stem = stem.split("\\")[-1]
|
||||
if stem.lower().endswith(".3dm"): stem = stem[:-4]
|
||||
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in stem)
|
||||
return "previews/" + safe + ".png"
|
||||
|
||||
|
||||
def _capture_thumbnail_of_objects(target_objects, png_abs_path, size=128):
|
||||
"""Captured einen Top-View der gegebenen Objekte als PNG. Hided
|
||||
temporaer alle anderen Objekte damit der Background sauber ist.
|
||||
Returns True/False."""
|
||||
if Rhino is None: return False
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None or not target_objects: return False
|
||||
try:
|
||||
from System.Drawing import Size
|
||||
import scriptcontext as sc
|
||||
except Exception:
|
||||
return False
|
||||
# IDs der Ziel-Objekte
|
||||
target_ids = set()
|
||||
for o in target_objects:
|
||||
try:
|
||||
if not o.IsDeleted: target_ids.add(str(o.Id))
|
||||
except Exception: pass
|
||||
if not target_ids: return False
|
||||
# Andere Objekte temporaer ausblenden
|
||||
hidden_by_us = []
|
||||
try:
|
||||
for o in list(doc.Objects):
|
||||
try:
|
||||
if o.IsDeleted: continue
|
||||
if str(o.Id) in target_ids: continue
|
||||
if o.IsHidden: continue
|
||||
if not o.IsNormal: continue # bereits hidden/locked → skip
|
||||
doc.Objects.Hide(o.Id, True)
|
||||
hidden_by_us.append(o.Id)
|
||||
except Exception: pass
|
||||
except Exception: pass
|
||||
capture_ok = False
|
||||
try:
|
||||
view = doc.Views.ActiveView
|
||||
if view is None:
|
||||
return False
|
||||
vp = view.ActiveViewport
|
||||
# Viewport-State sichern damit User nichts verliert
|
||||
saved_target = vp.CameraTarget
|
||||
saved_loc = vp.CameraLocation
|
||||
saved_proj = vp.IsParallelProjection
|
||||
try:
|
||||
# Auf Top-Parallel wechseln + Zoom auf Ziel
|
||||
vp.SetProjection(Rhino.Display.DefinedViewportProjection.Top, "Top", True)
|
||||
try:
|
||||
bbox = Rhino.Geometry.BoundingBox.Empty
|
||||
for o in target_objects:
|
||||
g = o.Geometry
|
||||
if g is None: continue
|
||||
try:
|
||||
bb = g.GetBoundingBox(True)
|
||||
if bb.IsValid: bbox.Union(bb)
|
||||
except Exception: pass
|
||||
if bbox.IsValid:
|
||||
# Etwas Padding
|
||||
bbox.Inflate(bbox.Diagonal.Length * 0.1)
|
||||
vp.ZoomBoundingBox(bbox)
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] thumbnail zoom:", ex)
|
||||
view.Redraw()
|
||||
# Capture
|
||||
try:
|
||||
bmp = view.CaptureToBitmap(Size(int(size), int(size)))
|
||||
if bmp is not None:
|
||||
# Sicherstellen dass Verzeichnis da ist
|
||||
try:
|
||||
d = os.path.dirname(png_abs_path)
|
||||
if d and not os.path.isdir(d): os.makedirs(d)
|
||||
except Exception: pass
|
||||
bmp.Save(png_abs_path)
|
||||
capture_ok = True
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] thumbnail capture:", ex)
|
||||
finally:
|
||||
# Viewport wiederherstellen
|
||||
try:
|
||||
vp.SetCameraLocation(saved_loc, False)
|
||||
vp.SetCameraTarget(saved_target, True)
|
||||
if saved_proj:
|
||||
vp.IsParallelProjection = True
|
||||
else:
|
||||
vp.IsParallelProjection = False
|
||||
except Exception: pass
|
||||
finally:
|
||||
# Hidden Objekte wieder einblenden
|
||||
for gid in hidden_by_us:
|
||||
try: doc.Objects.Show(gid, True)
|
||||
except Exception: pass
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
return capture_ok
|
||||
|
||||
|
||||
def read_preview_data_uri(rel_path):
|
||||
"""Liest die PNG-Vorschau als data:image/png;base64-URI fuer
|
||||
direktes Einsetzen in <img src='...'>. Liefert None wenn Datei fehlt."""
|
||||
if not rel_path: return None
|
||||
abs_p = os.path.join(library_root(), rel_path)
|
||||
if not os.path.isfile(abs_p): return None
|
||||
try:
|
||||
import base64
|
||||
with open(abs_p, "rb") as f:
|
||||
data = f.read()
|
||||
return "data:image/png;base64," + base64.b64encode(data).decode("ascii")
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] read_preview:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def convert_to_3dm_via_import(src_path, target_name):
|
||||
"""Konvertiert eine beliebige CAD-Datei (.dwg/.obj/.fbx/.dae/.stl/...)
|
||||
nach .3dm. Strategie: Rhinos _-Import in den aktiven Doc, dann die
|
||||
NEU hinzugekommenen Objekte als File3dm in library/assets/<name>.3dm
|
||||
speichern + aus dem Doc wieder loeschen. Returns relativer Pfad oder
|
||||
None.
|
||||
|
||||
WARNUNG: User-Doc wird kurz veraendert (Objects in/out) — wir
|
||||
delete'n alles wieder am Ende. Setzt einen sticky-Flag damit unsere
|
||||
eigenen Listener nichts cascaden."""
|
||||
if not src_path or not os.path.isfile(src_path): return None
|
||||
if Rhino is None: return None
|
||||
import scriptcontext as sc
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return None
|
||||
ensure_library()
|
||||
assets_dir = os.path.join(library_root(), "assets")
|
||||
if not os.path.isdir(assets_dir):
|
||||
try: os.makedirs(assets_dir)
|
||||
except Exception: pass
|
||||
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in target_name)
|
||||
if not safe.endswith(".3dm"): safe += ".3dm"
|
||||
target = os.path.join(assets_dir, safe)
|
||||
if os.path.isfile(target):
|
||||
stem, ext = os.path.splitext(safe)
|
||||
n = 2
|
||||
while os.path.isfile(os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))):
|
||||
n += 1
|
||||
target = os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))
|
||||
# Listener stilllegen: unsere Add-/Delete-Cascade soll bei diesem
|
||||
# temporaeren Import nicht greifen (Objekte haben keine DOSSIER-
|
||||
# UserStrings, kommen aber trotzdem durch unsere Schnellfilter).
|
||||
sc.sticky["dossier_library_import_busy"] = True
|
||||
sc.sticky["dossier_swisstopo_busy"] = True # blockt schon viele Listener
|
||||
# Snapshot der existierenden Object-IDs
|
||||
before_ids = set()
|
||||
try:
|
||||
for o in doc.Objects:
|
||||
try:
|
||||
if not o.IsDeleted: before_ids.add(str(o.Id))
|
||||
except Exception: pass
|
||||
except Exception: pass
|
||||
# User-Selection sichern damit wir sie am Ende restoren
|
||||
sel_before_ids = []
|
||||
try:
|
||||
for o in doc.Objects.GetSelectedObjects(False, False):
|
||||
sel_before_ids.append(o.Id)
|
||||
except Exception: pass
|
||||
new_objs = []
|
||||
try:
|
||||
try: doc.Objects.UnselectAll()
|
||||
except Exception: pass
|
||||
# _-Import dash-prefix = scripted, kein UI-Dialog. Pfad in Quotes
|
||||
# damit Spaces nicht splitten. _Enter beendet die Optionen.
|
||||
cmd = '_-Import "' + src_path + '" _Enter _Enter'
|
||||
try:
|
||||
Rhino.RhinoApp.RunScript(cmd, False)
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] convert_to_3dm RunScript:", ex)
|
||||
# Sammle die NEU hinzugekommenen Objekte
|
||||
try:
|
||||
for o in doc.Objects:
|
||||
try:
|
||||
if o.IsDeleted: continue
|
||||
if str(o.Id) not in before_ids:
|
||||
new_objs.append(o)
|
||||
except Exception: pass
|
||||
except Exception: pass
|
||||
if not new_objs:
|
||||
print("[LIBRARY] convert_to_3dm: Import lieferte keine Objekte")
|
||||
return None
|
||||
# In File3dm packen
|
||||
try:
|
||||
from Rhino.FileIO import File3dm
|
||||
import Rhino.Geometry as rg
|
||||
bbox = rg.BoundingBox.Empty
|
||||
geoms_attrs = []
|
||||
for o in new_objs:
|
||||
g = o.Geometry
|
||||
if g is None: continue
|
||||
try:
|
||||
bb = g.GetBoundingBox(True)
|
||||
if bb.IsValid: bbox.Union(bb)
|
||||
except Exception: pass
|
||||
geoms_attrs.append((g, o.Attributes))
|
||||
if bbox.IsValid:
|
||||
offset = rg.Transform.Translation(
|
||||
-bbox.Min.X, -bbox.Min.Y, -bbox.Min.Z)
|
||||
else:
|
||||
offset = rg.Transform.Identity
|
||||
f3 = File3dm()
|
||||
for g, a in geoms_attrs:
|
||||
try:
|
||||
g2 = g.Duplicate()
|
||||
try: g2.Transform(offset)
|
||||
except Exception: pass
|
||||
try: f3.Objects.Add(g2, a)
|
||||
except Exception:
|
||||
if isinstance(g2, rg.Brep): f3.Objects.AddBrep(g2)
|
||||
elif isinstance(g2, rg.Curve): f3.Objects.AddCurve(g2)
|
||||
elif isinstance(g2, rg.Mesh): f3.Objects.AddMesh(g2)
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] convert add geom:", ex)
|
||||
try: f3.Write(target, 8)
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] convert_to_3dm Write:", ex)
|
||||
return None
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] convert_to_3dm File3dm:", ex)
|
||||
return None
|
||||
finally:
|
||||
# Thumbnail-Capture BEVOR die Objekte geloescht werden.
|
||||
try:
|
||||
rel_for_preview = os.path.relpath(target, library_root())
|
||||
preview_rel = _preview_rel_for(rel_for_preview)
|
||||
preview_abs = os.path.join(library_root(), preview_rel)
|
||||
_capture_thumbnail_of_objects(new_objs, preview_abs)
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] convert thumbnail:", ex)
|
||||
# Cleanup: importierte Objekte wieder loeschen
|
||||
for o in new_objs:
|
||||
try: doc.Objects.Delete(o.Id, True)
|
||||
except Exception: pass
|
||||
# Restore Selection
|
||||
try:
|
||||
for gid in sel_before_ids:
|
||||
try: doc.Objects.Select(gid, True)
|
||||
except Exception: pass
|
||||
except Exception: pass
|
||||
rel = os.path.relpath(target, library_root())
|
||||
print("[LIBRARY] convert_to_3dm OK: {} → {}".format(src_path, rel))
|
||||
return rel
|
||||
finally:
|
||||
sc.sticky["dossier_library_import_busy"] = False
|
||||
sc.sticky["dossier_swisstopo_busy"] = False
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
|
||||
|
||||
def save_selection_to_asset(doc, target_name):
|
||||
"""Speichert die aktuelle Selection aus dem Doc als eigene .3dm-Datei
|
||||
in library/assets/<target_name>.3dm. Returns relativer Pfad oder None.
|
||||
Geometry wird relativ zum BoundingBox-Min platziert damit der Block-
|
||||
Origin am Ursprung sitzt — sinnvoll fuer Symbol-Insert."""
|
||||
if doc is None or not target_name: return None
|
||||
try:
|
||||
sel = list(doc.Objects.GetSelectedObjects(False, False))
|
||||
except Exception:
|
||||
sel = []
|
||||
if not sel:
|
||||
print("[LIBRARY] save_selection: keine Auswahl")
|
||||
return None
|
||||
if Rhino is None: return None
|
||||
ensure_library()
|
||||
assets_dir = os.path.join(library_root(), "assets")
|
||||
if not os.path.isdir(assets_dir):
|
||||
try: os.makedirs(assets_dir)
|
||||
except Exception: pass
|
||||
# Safe filename
|
||||
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in target_name)
|
||||
if not safe.endswith(".3dm"): safe += ".3dm"
|
||||
target = os.path.join(assets_dir, safe)
|
||||
if os.path.isfile(target):
|
||||
stem, ext = os.path.splitext(safe)
|
||||
n = 2
|
||||
while os.path.isfile(os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))):
|
||||
n += 1
|
||||
target = os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))
|
||||
# File3dm aufbauen + Selection rein
|
||||
try:
|
||||
from Rhino.FileIO import File3dm, File3dmWriteOptions
|
||||
import Rhino.Geometry as rg
|
||||
# BoundingBox sammeln um geometrie an Ursprung zu verschieben
|
||||
bbox = rg.BoundingBox.Empty
|
||||
geoms = []
|
||||
for o in sel:
|
||||
g = o.Geometry
|
||||
if g is None: continue
|
||||
try:
|
||||
bb = g.GetBoundingBox(True)
|
||||
if bb.IsValid: bbox.Union(bb)
|
||||
except Exception: pass
|
||||
geoms.append((g, o.Attributes))
|
||||
if not geoms:
|
||||
return None
|
||||
if not bbox.IsValid:
|
||||
origin = rg.Point3d(0, 0, 0)
|
||||
else:
|
||||
origin = bbox.Min
|
||||
offset = rg.Transform.Translation(-origin.X, -origin.Y, -origin.Z)
|
||||
f3 = File3dm()
|
||||
for g, a in geoms:
|
||||
try:
|
||||
g2 = g.Duplicate()
|
||||
try: g2.Transform(offset)
|
||||
except Exception: pass
|
||||
# Generischer Add fuer alle GeometryBase
|
||||
try: f3.Objects.Add(g2, a)
|
||||
except Exception:
|
||||
# Fallback: typ-spezifisch
|
||||
if isinstance(g2, rg.Brep): f3.Objects.AddBrep(g2)
|
||||
elif isinstance(g2, rg.Curve): f3.Objects.AddCurve(g2)
|
||||
elif isinstance(g2, rg.Mesh): f3.Objects.AddMesh(g2)
|
||||
elif isinstance(g2, rg.Point): f3.Objects.AddPoint(g2.Location)
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] save_selection add:", ex)
|
||||
# Write
|
||||
try:
|
||||
opts = File3dmWriteOptions()
|
||||
opts.Version = 8
|
||||
f3.Write(target, opts)
|
||||
except Exception:
|
||||
try: f3.Write(target, 8)
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] save_selection write:", ex)
|
||||
return None
|
||||
rel = os.path.relpath(target, library_root())
|
||||
# Thumbnail aus den (noch selektierten) Objekten capturen
|
||||
try:
|
||||
preview_rel = _preview_rel_for(rel)
|
||||
preview_abs = os.path.join(library_root(), preview_rel)
|
||||
_capture_thumbnail_of_objects(sel, preview_abs)
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] save_selection thumbnail:", ex)
|
||||
print("[LIBRARY] save_selection: {} objs → {}".format(len(geoms), rel))
|
||||
return rel
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] save_selection:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def copy_to_assets(src_path, target_name=None):
|
||||
"""Kopiert eine Datei in library/assets/. target_name optional (sonst
|
||||
Original-Name). Returns relativer Pfad ('assets/foo.3dm') oder None."""
|
||||
if not src_path or not os.path.isfile(src_path):
|
||||
return None
|
||||
ensure_library()
|
||||
assets_dir = os.path.join(library_root(), "assets")
|
||||
if not os.path.isdir(assets_dir):
|
||||
try: os.makedirs(assets_dir)
|
||||
except Exception: pass
|
||||
base = target_name or os.path.basename(src_path)
|
||||
target = os.path.join(assets_dir, base)
|
||||
# Konflikt-Resolution: bei doppeltem Namen Nummer dran
|
||||
if os.path.isfile(target):
|
||||
stem, ext = os.path.splitext(base)
|
||||
n = 2
|
||||
while os.path.isfile(os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))):
|
||||
n += 1
|
||||
target = os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))
|
||||
try:
|
||||
import shutil
|
||||
shutil.copy2(src_path, target)
|
||||
rel = os.path.relpath(target, library_root())
|
||||
return rel
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] copy_to_assets:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def _empty_manifest():
|
||||
return {"schemaVersion": _SCHEMA_VERSION,
|
||||
"name": "Dossier-Library", "items": []}
|
||||
|
||||
|
||||
def _normalize_item(it):
|
||||
if not isinstance(it, dict): return None
|
||||
if not it.get("id") or not it.get("type"): return None
|
||||
# 2D/3D-Files: ein Item kann beide haben (Objekt mit Plan-Darstellung 2D
|
||||
# + perspektivischem 3D-Modell) oder nur eines. Legacy 'files'-Feld wird
|
||||
# als files2d interpretiert (Symbole = nur 2D historisch).
|
||||
files_legacy = list(it.get("files") or [])
|
||||
files2d = list(it.get("files2d") or [])
|
||||
files3d = list(it.get("files3d") or [])
|
||||
if not files2d and not files3d and files_legacy:
|
||||
files2d = files_legacy
|
||||
out = {
|
||||
"id": str(it["id"]),
|
||||
"type": str(it["type"]),
|
||||
"name": str(it.get("name") or "Unbenannt"),
|
||||
"version": int(it.get("version") or 1),
|
||||
"tags": list(it.get("tags") or []),
|
||||
"preview": it.get("preview"),
|
||||
"data": it.get("data") or {},
|
||||
"files2d": files2d,
|
||||
"files3d": files3d,
|
||||
# legacy "files" mitgeben fuer Backwards-Kompatibilitaet
|
||||
"files": files_legacy or files2d,
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def find_item(item_id):
|
||||
"""Sucht ein Item per id im Manifest. Returns None wenn nicht da."""
|
||||
if not item_id: return None
|
||||
for it in load_manifest().get("items", []):
|
||||
if it.get("id") == item_id: return it
|
||||
return None
|
||||
|
||||
|
||||
# --- Import-Logik -----------------------------------------------------------
|
||||
|
||||
def import_material(doc, item):
|
||||
"""Importiert ein Material-Item in die Projekt-Settings des Doc. Dedupe
|
||||
per libraryId — wenn schon importiert, kein Doppel-Eintrag.
|
||||
Returns (ok, message)."""
|
||||
if doc is None: return False, "Kein aktives Dokument"
|
||||
if not isinstance(item, dict) or item.get("type") != "material":
|
||||
return False, "Item ist kein Material"
|
||||
data = item.get("data") or {}
|
||||
new_mat = {
|
||||
"name": item.get("name") or "Unbenannt",
|
||||
"color": data.get("color", "#888888"),
|
||||
"source": "library",
|
||||
"libraryId": item.get("id"),
|
||||
}
|
||||
# PBR + Textur-Felder, falls Library-Item welche hat
|
||||
for k in ("roughness", "reflection", "transparency", "iorN",
|
||||
"uvScaleM", "textures"):
|
||||
if k in data: new_mat[k] = data[k]
|
||||
# Lazy-Import um Zyklen zu vermeiden
|
||||
import layers_panel as rhinopanel
|
||||
settings = rhinopanel.load_project_settings(doc)
|
||||
mats = list(settings.get("materials", []))
|
||||
for m in mats:
|
||||
if m.get("libraryId") == new_mat["libraryId"]:
|
||||
return False, "Material bereits importiert"
|
||||
mats.append(new_mat)
|
||||
settings["materials"] = mats
|
||||
rhinopanel.save_project_settings(doc, settings)
|
||||
return True, "Material importiert"
|
||||
|
||||
|
||||
# --- Symbol/Object-Import via File3dm + InstanceDefinitions ----------------
|
||||
|
||||
def _lib_asset_path(rel_path):
|
||||
"""Absoluter Pfad zu einer Library-Asset-Datei (Eintrag aus item.files)."""
|
||||
if not rel_path: return None
|
||||
return os.path.join(library_root(), rel_path)
|
||||
|
||||
|
||||
def _block_name_for(item, variant=""):
|
||||
"""Stabiler Block-Name fuer InstanceDefinition. Format:
|
||||
'dossier_lib_<libraryId>[_<variant>]'. variant '2d'/'3d' fuer Pair-Items."""
|
||||
lid = item.get("id") or ""
|
||||
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in lid)
|
||||
name = "dossier_lib_" + safe
|
||||
if variant: name += "_" + variant
|
||||
return name
|
||||
|
||||
|
||||
def _read_3dm_geometry(abs_path):
|
||||
"""Liest alle Top-Level-Objekte aus einer .3dm-Datei. Returns Liste von
|
||||
(GeometryBase, ObjectAttributes). Bei Fehler leere Liste."""
|
||||
if not abs_path or not os.path.isfile(abs_path):
|
||||
print("[LIBRARY] _read_3dm: Datei not found:", abs_path)
|
||||
return []
|
||||
try:
|
||||
from Rhino.FileIO import File3dm
|
||||
f3 = File3dm.Read(abs_path)
|
||||
if f3 is None:
|
||||
print("[LIBRARY] _read_3dm: File3dm.Read returned None:", abs_path)
|
||||
return []
|
||||
out = []
|
||||
for obj in f3.Objects:
|
||||
try:
|
||||
g = obj.Geometry
|
||||
a = obj.Attributes.Duplicate() if obj.Attributes else None
|
||||
if g is not None:
|
||||
out.append((g.Duplicate(), a))
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] _read_3dm obj:", ex)
|
||||
return out
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] _read_3dm:", ex)
|
||||
return []
|
||||
|
||||
|
||||
def _ensure_block_definition(doc, item, geometry_attrs, variant=""):
|
||||
"""Erstellt InstanceDefinition fuer dieses Library-Item wenn noch nicht
|
||||
da. variant unterscheidet '2d'/'3d'. Returns (idx, was_created)."""
|
||||
if Rhino is None: return -1, False
|
||||
name = _block_name_for(item, variant=variant)
|
||||
try:
|
||||
existing = doc.InstanceDefinitions.Find(name)
|
||||
except Exception:
|
||||
existing = None
|
||||
if existing is not None:
|
||||
return existing.Index, False
|
||||
geoms = [g for g, _ in geometry_attrs if g is not None]
|
||||
attrs = [a for _, a in geometry_attrs]
|
||||
if not geoms:
|
||||
return -1, False
|
||||
base_pt = Rhino.Geometry.Point3d(0, 0, 0)
|
||||
desc_suffix = " ({})".format(variant.upper()) if variant else ""
|
||||
desc = "Dossier-Library: " + (item.get("name") or "") + desc_suffix
|
||||
try:
|
||||
idx = doc.InstanceDefinitions.Add(name, desc, base_pt, geoms, attrs)
|
||||
return idx, True
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] InstanceDef Add:", ex)
|
||||
return -1, False
|
||||
|
||||
|
||||
def _build_variant_block(doc, item, variant_files, variant_label):
|
||||
"""Liest .3dm-Files und legt eine InstanceDefinition (variant) an.
|
||||
Returns Block-Index oder -1."""
|
||||
if not variant_files: return -1
|
||||
all_geom = []
|
||||
for f in variant_files:
|
||||
abs_p = _lib_asset_path(f)
|
||||
all_geom.extend(_read_3dm_geometry(abs_p))
|
||||
if not all_geom:
|
||||
return -1
|
||||
idx, _ = _ensure_block_definition(doc, item, all_geom, variant=variant_label)
|
||||
return idx
|
||||
|
||||
|
||||
def _place_instance(doc, block_idx, point, layer_idx=-1):
|
||||
"""Platziert eine InstanceObject am gegebenen Punkt, optional auf
|
||||
spezifischem Layer. Returns Guid oder None."""
|
||||
if block_idx < 0: return None
|
||||
try:
|
||||
xform = Rhino.Geometry.Transform.Translation(
|
||||
point.X, point.Y, point.Z)
|
||||
attrs = Rhino.DocObjects.ObjectAttributes()
|
||||
if layer_idx >= 0:
|
||||
attrs.LayerIndex = layer_idx
|
||||
gid = doc.Objects.AddInstanceObject(block_idx, xform, attrs)
|
||||
return gid
|
||||
except Exception as ex:
|
||||
print("[LIBRARY] _place_instance:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def import_symbol(doc, item, at_point=None, layer2d=-1, layer3d=-1):
|
||||
return _import_block_like(doc, item, at_point=at_point,
|
||||
layer2d=layer2d, layer3d=layer3d)
|
||||
|
||||
|
||||
def import_object(doc, item, at_point=None, layer2d=-1, layer3d=-1):
|
||||
return _import_block_like(doc, item, at_point=at_point,
|
||||
layer2d=layer2d, layer3d=layer3d)
|
||||
|
||||
|
||||
def _import_block_like(doc, item, at_point=None, layer2d=-1, layer3d=-1):
|
||||
"""Platziert ein Library-Item im Doc. Item kann files2d und/oder files3d
|
||||
haben → beide Varianten werden geladen + an gleichem Punkt platziert auf
|
||||
ihren respektiven Layern.
|
||||
at_point: Point3d (default = Origin).
|
||||
layer2d/3d: optionale Layer-Indizes (default = aktiver Layer)."""
|
||||
if doc is None: return False, "Kein aktives Dokument"
|
||||
files2d = item.get("files2d") or []
|
||||
files3d = item.get("files3d") or []
|
||||
if not files2d and not files3d:
|
||||
# Legacy fallback
|
||||
legacy = item.get("files") or []
|
||||
if legacy: files2d = legacy
|
||||
if not files2d and not files3d:
|
||||
return False, "Item hat keine .3dm-Files: " + str(item.get("id"))
|
||||
if at_point is None:
|
||||
at_point = Rhino.Geometry.Point3d(0, 0, 0)
|
||||
placed = []
|
||||
if files2d:
|
||||
idx2 = _build_variant_block(doc, item, files2d, "2d")
|
||||
if idx2 >= 0:
|
||||
gid = _place_instance(doc, idx2, at_point, layer_idx=layer2d)
|
||||
if gid is not None and gid != System_Guid_Empty():
|
||||
placed.append("2D")
|
||||
if files3d:
|
||||
idx3 = _build_variant_block(doc, item, files3d, "3d")
|
||||
if idx3 >= 0:
|
||||
gid = _place_instance(doc, idx3, at_point, layer_idx=layer3d)
|
||||
if gid is not None and gid != System_Guid_Empty():
|
||||
placed.append("3D")
|
||||
if not placed:
|
||||
return False, "Konnte keinen Block platzieren (Files fehlen?)"
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
return True, "{} eingefuegt ({})".format(
|
||||
item.get("name") or "", "+".join(placed))
|
||||
|
||||
|
||||
def System_Guid_Empty():
|
||||
"""Helper — System.Guid.Empty Vergleichswert."""
|
||||
try:
|
||||
import System
|
||||
return System.Guid.Empty
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def import_item(doc, item_id, at_point=None, layer2d=-1, layer3d=-1):
|
||||
"""Type-dispatching Import. material → Project-Settings-Liste.
|
||||
symbol/object → InstanceDefinition im Doc via File3dm.Read.
|
||||
at_point + layer2d/3d nur fuer symbol/object."""
|
||||
item = find_item(item_id)
|
||||
if item is None: return False, "Item not found: " + str(item_id)
|
||||
t = item.get("type")
|
||||
if t == "material":
|
||||
return import_material(doc, item)
|
||||
if t == "symbol":
|
||||
return import_symbol(doc, item, at_point=at_point,
|
||||
layer2d=layer2d, layer3d=layer3d)
|
||||
if t == "object":
|
||||
return import_object(doc, item, at_point=at_point,
|
||||
layer2d=layer2d, layer3d=layer3d)
|
||||
return False, "Unbekannter Typ: '{}'".format(t)
|
||||
@@ -0,0 +1,210 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
mass_style.py
|
||||
Globale Mass-Stil-Presets fuer Dossier — speichert pro Dokument benannte
|
||||
Konfigurationen fuer:
|
||||
- Raum-Flaechen-Rundung
|
||||
- Mass-Linien-Dezimalstellen
|
||||
- (erweiterbar)
|
||||
|
||||
Persistiert als JSON in doc.Strings["dossier_mass_styles"] (Liste) und
|
||||
doc.Strings["dossier_mass_style_active"] (aktive ID).
|
||||
|
||||
Ein Mass-Style wird als globale Vorgabe gelesen. Per-Element-Override
|
||||
(z.B. raum_rundung UserString am einzelnen Raum) hat Vorrang wenn set.
|
||||
"""
|
||||
import json
|
||||
import uuid
|
||||
import Rhino
|
||||
|
||||
_KEY_STYLES = "dossier_mass_styles"
|
||||
_KEY_ACTIVE = "dossier_mass_style_active"
|
||||
|
||||
_RAUM_RUNDUNGEN = ("exakt", "0.01", "0.1", "0.5", "1")
|
||||
_DIM_DEZIMALSTELLEN = (0, 1, 2, 3, 4)
|
||||
_DIM_EINHEITEN = ("m", "cm", "mm")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Default-Presets — werden lazy beim ersten Lesen erzeugt wenn das Doc
|
||||
# noch keine Mass-Styles kennt. Decken die gaengigen Massstaebe ab.
|
||||
|
||||
_DEFAULT_PRESETS = [
|
||||
{
|
||||
"name": "Plan 1:50",
|
||||
"raumRundung": "0.1",
|
||||
"dimDezimalstellen": 2,
|
||||
"dimEinheit": "m",
|
||||
"default": True,
|
||||
},
|
||||
{
|
||||
"name": "Plan 1:100",
|
||||
"raumRundung": "0.5",
|
||||
"dimDezimalstellen": 2,
|
||||
"dimEinheit": "m",
|
||||
"default": False,
|
||||
},
|
||||
{
|
||||
"name": "Übersicht 1:500",
|
||||
"raumRundung": "1",
|
||||
"dimDezimalstellen": 1,
|
||||
"dimEinheit": "m",
|
||||
"default": False,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _normalize(preset):
|
||||
"""Validiert + bereinigt einen Preset-Eintrag (Defaults setzen,
|
||||
nicht-erlaubte Werte korrigieren)."""
|
||||
if not isinstance(preset, dict):
|
||||
preset = {}
|
||||
rr = preset.get("raumRundung") or "0.1"
|
||||
if rr not in _RAUM_RUNDUNGEN: rr = "0.1"
|
||||
try:
|
||||
dd = int(preset.get("dimDezimalstellen", 2))
|
||||
except (TypeError, ValueError):
|
||||
dd = 2
|
||||
if dd not in _DIM_DEZIMALSTELLEN: dd = 2
|
||||
de = preset.get("dimEinheit") or "m"
|
||||
if de not in _DIM_EINHEITEN: de = "m"
|
||||
return {
|
||||
"id": preset.get("id") or ("ms_" + uuid.uuid4().hex[:8]),
|
||||
"name": preset.get("name") or "Unbenannt",
|
||||
"raumRundung": rr,
|
||||
"dimDezimalstellen": dd,
|
||||
"dimEinheit": de,
|
||||
}
|
||||
|
||||
|
||||
def list_presets(doc):
|
||||
if doc is None: return []
|
||||
try:
|
||||
raw = doc.Strings.GetValue(_KEY_STYLES)
|
||||
if not raw:
|
||||
# Erst-Initialisierung: Default-Liste schreiben
|
||||
items = [_normalize(p) for p in _DEFAULT_PRESETS]
|
||||
_save_all(doc, items)
|
||||
# Default-Aktiv setzen falls noch nichts set
|
||||
if not doc.Strings.GetValue(_KEY_ACTIVE):
|
||||
doc.Strings.SetString(_KEY_ACTIVE, items[0]["id"])
|
||||
return items
|
||||
parsed = json.loads(raw)
|
||||
if not isinstance(parsed, list): return []
|
||||
return [_normalize(p) for p in parsed]
|
||||
except Exception as ex:
|
||||
print("[MASS_STYLE] list:", ex)
|
||||
return []
|
||||
|
||||
|
||||
def _save_all(doc, items):
|
||||
try:
|
||||
doc.Strings.SetString(_KEY_STYLES, json.dumps(items))
|
||||
except Exception as ex:
|
||||
print("[MASS_STYLE] save:", ex)
|
||||
|
||||
|
||||
def get_active_id(doc):
|
||||
if doc is None: return None
|
||||
try:
|
||||
v = doc.Strings.GetValue(_KEY_ACTIVE)
|
||||
return v or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def set_active_id(doc, preset_id):
|
||||
if doc is None: return
|
||||
items = list_presets(doc)
|
||||
if preset_id and not any(p["id"] == preset_id for p in items):
|
||||
return # Unbekannte ID — nicht setzen
|
||||
try:
|
||||
doc.Strings.SetString(_KEY_ACTIVE, preset_id or "")
|
||||
except Exception as ex:
|
||||
print("[MASS_STYLE] set active:", ex)
|
||||
|
||||
|
||||
def get_active(doc):
|
||||
"""Liefert das aktive Preset (dict) oder None."""
|
||||
items = list_presets(doc)
|
||||
aid = get_active_id(doc)
|
||||
if aid:
|
||||
for p in items:
|
||||
if p["id"] == aid: return p
|
||||
# Fallback: erstes Preset (oder None wenn leer)
|
||||
return items[0] if items else None
|
||||
|
||||
|
||||
def save_preset(doc, preset):
|
||||
"""Speichert/aktualisiert ein Preset. Wenn `id` im preset existiert
|
||||
und in der Liste ist → Update, sonst Append. Returns die finale ID."""
|
||||
if doc is None: return None
|
||||
items = list_presets(doc)
|
||||
norm = _normalize(preset)
|
||||
pid = preset.get("id") if isinstance(preset, dict) else None
|
||||
if pid:
|
||||
replaced = False
|
||||
for i, p in enumerate(items):
|
||||
if p["id"] == pid:
|
||||
norm["id"] = pid
|
||||
items[i] = norm
|
||||
replaced = True
|
||||
break
|
||||
if not replaced:
|
||||
items.append(norm)
|
||||
else:
|
||||
items.append(norm)
|
||||
_save_all(doc, items)
|
||||
return norm["id"]
|
||||
|
||||
|
||||
def delete_preset(doc, preset_id):
|
||||
if doc is None or not preset_id: return
|
||||
items = [p for p in list_presets(doc) if p["id"] != preset_id]
|
||||
_save_all(doc, items)
|
||||
# Wenn aktives Preset geloescht: auf erstes uebriges umschalten
|
||||
if get_active_id(doc) == preset_id:
|
||||
set_active_id(doc, items[0]["id"] if items else "")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Convenience: Defaults fuer Module die das Preset einlesen
|
||||
|
||||
def raum_rundung_default(doc):
|
||||
"""Default-Rundung fuer Raum-Stempel wenn keine per-Raum-Override
|
||||
set ist."""
|
||||
p = get_active(doc)
|
||||
return p["raumRundung"] if p else "0.1"
|
||||
|
||||
|
||||
def dim_dezimalstellen_default(doc):
|
||||
p = get_active(doc)
|
||||
return p["dimDezimalstellen"] if p else 2
|
||||
|
||||
|
||||
def dim_einheit_default(doc):
|
||||
p = get_active(doc)
|
||||
return p["dimEinheit"] if p else "m"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cross-Module: Raum-Stempel beim Preset-Wechsel mit-aktualisieren.
|
||||
|
||||
def regen_all_rooms(doc):
|
||||
"""Queued ein Regen fuer ALLE raum_outline-Objekte. Aufruf bei Preset-
|
||||
Wechsel/Save/Delete damit Stempel-Flaechen in der neuen Rundung neu
|
||||
rendern."""
|
||||
if doc is None: return
|
||||
try:
|
||||
import elemente
|
||||
except Exception as ex:
|
||||
print("[MASS_STYLE] elemente import:", ex); return
|
||||
for obj in doc.Objects:
|
||||
try:
|
||||
m = elemente._read_meta(obj)
|
||||
if m and m.get("type") == "raum_outline":
|
||||
elemente._queue_regen(m["id"])
|
||||
except Exception: pass
|
||||
@@ -0,0 +1,87 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
masse_settings.py
|
||||
Satellite-Fenster fuer das Bearbeiten der Masse-Presets
|
||||
(rhino/mass_style.py). Vom Oberleiste-Zahnrad geoeffnet.
|
||||
"""
|
||||
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 mass_style
|
||||
|
||||
|
||||
def _payload():
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
return {
|
||||
"presets": mass_style.list_presets(doc),
|
||||
"activeId": mass_style.get_active_id(doc),
|
||||
}
|
||||
|
||||
|
||||
def _notify_oberleiste():
|
||||
"""Topbar nach Aenderung refreshen — die zeigt den aktiven Preset im
|
||||
Dropdown an."""
|
||||
try:
|
||||
b = sc.sticky.get("oberleiste_bridge")
|
||||
if b is not None:
|
||||
b._send_state(force=True)
|
||||
except Exception as ex:
|
||||
print("[MASSE_SETTINGS] notify oberleiste:", ex)
|
||||
|
||||
|
||||
class MasseSettingsBridge(panel_base.BaseBridge):
|
||||
def __init__(self):
|
||||
panel_base.BaseBridge.__init__(self, "masse_settings")
|
||||
|
||||
def _on_ready(self):
|
||||
self._send_state()
|
||||
|
||||
def _send_state(self):
|
||||
self.send("STATE", _payload())
|
||||
|
||||
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_ACTIVE":
|
||||
mass_style.set_active_id(doc, p.get("id"))
|
||||
mass_style.regen_all_rooms(doc)
|
||||
self._send_state()
|
||||
_notify_oberleiste()
|
||||
elif t == "SAVE":
|
||||
mass_style.save_preset(doc, p.get("preset") or {})
|
||||
mass_style.regen_all_rooms(doc)
|
||||
self._send_state()
|
||||
_notify_oberleiste()
|
||||
elif t == "DELETE":
|
||||
mass_style.delete_preset(doc, p.get("id"))
|
||||
mass_style.regen_all_rooms(doc)
|
||||
self._send_state()
|
||||
_notify_oberleiste()
|
||||
|
||||
|
||||
def open_as_window():
|
||||
"""Oeffnet das Masse-Settings-Fenster (Eto.Form + WebView).
|
||||
Vom Oberleiste-Zahnrad bei OPEN_MASSE_SETTINGS aufgerufen."""
|
||||
b = MasseSettingsBridge()
|
||||
sc.sticky["masse_settings_bridge"] = b
|
||||
panel_base.open_satellite_window(
|
||||
"masse_settings",
|
||||
title="Masse",
|
||||
size=(440, 520),
|
||||
bridge=b)
|
||||
+65
-54
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
massstab.py
|
||||
MASSSTAB-Panel: zeigt + setzt den aktuellen Massstab des aktiven Viewports.
|
||||
@@ -32,11 +34,11 @@ PANEL_GUID_STR = "5c8e4f3f-6d0e-4f1a-a3d4-e5f607182941"
|
||||
# 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
|
||||
# Pro Viewport-Name der zuletzt vom User explizit sete 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
|
||||
# JSON-Dict, damit ein Wechsel zurueck auf einen frueher seten 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
|
||||
@@ -102,7 +104,7 @@ def _detect_dpi():
|
||||
try:
|
||||
from System.Diagnostics import Process, ProcessStartInfo
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] auto-detect: .NET Process nicht verfuegbar:", ex)
|
||||
print("[SCALE] auto-detect: .NET Process not available:", ex)
|
||||
return None
|
||||
if not os.path.isfile("/usr/bin/osascript"):
|
||||
# Vermutlich nicht macOS -> nichts zu detecten
|
||||
@@ -135,10 +137,10 @@ def _detect_dpi():
|
||||
if not finished:
|
||||
try: p.Kill()
|
||||
except Exception: pass
|
||||
print("[MASSSTAB] auto-detect: osascript timeout")
|
||||
print("[SCALE] auto-detect: osascript timeout")
|
||||
return None
|
||||
if p.ExitCode != 0:
|
||||
print("[MASSSTAB] auto-detect osascript ExitCode={}:".format(p.ExitCode), err)
|
||||
print("[SCALE] auto-detect osascript ExitCode={}:".format(p.ExitCode), err)
|
||||
return None
|
||||
import json as _json
|
||||
data = _json.loads((out or "").strip())
|
||||
@@ -152,15 +154,15 @@ def _detect_dpi():
|
||||
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))
|
||||
print("[SCALE] 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(
|
||||
print("[SCALE] DPI auto-detected: {:.1f} physical (screen {:.0f}x{:.0f}px / ... logical {:.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)
|
||||
print("[SCALE] auto-detect failed:", ex)
|
||||
return None
|
||||
finally:
|
||||
if script_path:
|
||||
@@ -186,7 +188,7 @@ def _read_config():
|
||||
if isinstance(data, dict):
|
||||
cfg = data
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] config lesen:", ex)
|
||||
print("[SCALE] config lesen:", ex)
|
||||
_config_cache = cfg
|
||||
return cfg
|
||||
|
||||
@@ -202,7 +204,7 @@ def _write_config(cfg):
|
||||
_config_cache = cfg # Cache mit dem geschriebenen Stand aktualisieren
|
||||
return True
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] config schreiben:", ex)
|
||||
print("[SCALE] config schreiben:", ex)
|
||||
return False
|
||||
|
||||
|
||||
@@ -264,7 +266,7 @@ def _set_dpi(doc, value, source="manual"):
|
||||
cfg["dpi_source"] = source
|
||||
if not _write_config(cfg):
|
||||
return False
|
||||
print("[MASSSTAB] DPI={:.1f} ({}) -> {}".format(v, source, _CONFIG_PATH))
|
||||
print("[SCALE] DPI={:.1f} ({}) -> {}".format(v, source, _CONFIG_PATH))
|
||||
return True
|
||||
|
||||
|
||||
@@ -346,7 +348,7 @@ def _compute_scale(doc, vp):
|
||||
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.
|
||||
# Setzen, kommt entweder dessen frueher seter 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
|
||||
@@ -419,9 +421,9 @@ def _apply_scaled_lineweights(doc, enabled, scale_n):
|
||||
layer.PlotWeight = new
|
||||
n_layer += 1
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] LW scale layer '{}': {}".format(layer.Name, ex))
|
||||
print("[SCALE] LW scale layer '{}': {}".format(layer.Name, ex))
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] LW scale layers:", ex)
|
||||
print("[SCALE] LW scale layers:", ex)
|
||||
|
||||
# -- Objekte -------------------------------------------------------------
|
||||
try:
|
||||
@@ -449,11 +451,11 @@ def _apply_scaled_lineweights(doc, enabled, scale_n):
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] LW scale objects:", ex)
|
||||
print("[SCALE] LW scale objects:", ex)
|
||||
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
print("[MASSSTAB] PlotWeight-Skalierung x{:.1f}: {} Layer, {} Objekte angepasst".format(
|
||||
print("[SCALE] PlotWeight-Skalierung x{:.1f}: {} Layer, {} Objekte angepasst".format(
|
||||
factor, n_layer, n_obj))
|
||||
# Diagnose: zeige die ersten paar Layer mit ihren echten PlotWeights
|
||||
try:
|
||||
@@ -461,7 +463,7 @@ def _apply_scaled_lineweights(doc, enabled, scale_n):
|
||||
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(
|
||||
print("[SCALE] Layer '{}' PlotWeight={:.3f}mm (orig={})".format(
|
||||
layer.Name, float(layer.PlotWeight), stored))
|
||||
shown += 1
|
||||
if shown >= 5: break
|
||||
@@ -477,7 +479,7 @@ def write_plotweight(doc, target, value):
|
||||
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.
|
||||
PlotWeight = value * scale set damit die Anzeige direkt skaliert.
|
||||
|
||||
Aufrufer ist verantwortlich fuer ModifyAttributes / Doc-Refresh."""
|
||||
if target is None: return
|
||||
@@ -497,7 +499,7 @@ def write_plotweight(doc, target, value):
|
||||
try:
|
||||
target.PlotWeight = v * factor
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] write_plotweight set:", ex)
|
||||
print("[SCALE] write_plotweight set:", ex)
|
||||
|
||||
|
||||
def apply_scaled_hatches(doc, scale_n):
|
||||
@@ -570,11 +572,11 @@ def apply_scaled_hatches(doc, scale_n):
|
||||
if doc.Objects.Replace(hid, new_g):
|
||||
n_scaled += 1
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] hatch set PatternScale:", ex)
|
||||
print("[SCALE] hatch set PatternScale:", ex)
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] hatch iter:", ex)
|
||||
print("[SCALE] hatch iter:", ex)
|
||||
if n_scaled or hatch_ids:
|
||||
print("[MASSSTAB] Hatch-Skalierung: {} gefunden, {} mit Faktor x{:.2f} angepasst".format(
|
||||
print("[SCALE] Hatch-Skalierung: {} gefunden, {} mit Faktor x{:.2f} angepasst".format(
|
||||
len(hatch_ids), n_scaled, factor))
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
@@ -597,7 +599,7 @@ def post_create_hatch_scale(doc, hatch_obj, user_scale):
|
||||
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)
|
||||
print("[SCALE] 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
|
||||
@@ -610,7 +612,7 @@ def post_create_hatch_scale(doc, hatch_obj, user_scale):
|
||||
new_g.PatternScale = u * factor
|
||||
doc.Objects.Replace(h2.Id, new_g)
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] post_create_hatch_scale rescale:", ex)
|
||||
print("[SCALE] post_create_hatch_scale rescale:", ex)
|
||||
|
||||
|
||||
def read_plotweight(target):
|
||||
@@ -654,7 +656,7 @@ def _set_lineweights_enabled(doc, enabled):
|
||||
try:
|
||||
doc.Strings.SetString(_LW_KEY, flag)
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] _set_lineweights_enabled persist:", ex)
|
||||
print("[SCALE] _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"
|
||||
@@ -673,17 +675,17 @@ def _set_lineweights_enabled(doc, enabled):
|
||||
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)
|
||||
print("[SCALE] 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")
|
||||
print("[SCALE] 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."""
|
||||
um "nie set" zu erkennen."""
|
||||
if doc is None: return default
|
||||
try:
|
||||
raw = doc.Strings.GetValue(_DOC_USER_SCALE_KEY)
|
||||
@@ -700,7 +702,7 @@ def _write_user_scale(doc, ratio):
|
||||
try:
|
||||
doc.Strings.SetString(_DOC_USER_SCALE_KEY, "{:.6f}".format(float(ratio)))
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] _write_user_scale:", ex)
|
||||
print("[SCALE] _write_user_scale:", ex)
|
||||
|
||||
|
||||
def _ensure_user_scales_loaded(doc):
|
||||
@@ -721,7 +723,7 @@ def _ensure_user_scales_loaded(doc):
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] _ensure_user_scales_loaded:", ex)
|
||||
print("[SCALE] _ensure_user_scales_loaded:", ex)
|
||||
_user_set_scales_loaded = True
|
||||
|
||||
|
||||
@@ -731,7 +733,7 @@ def _write_user_scales(doc):
|
||||
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)
|
||||
print("[SCALE] _write_user_scales:", ex)
|
||||
|
||||
|
||||
def _get_applied_scale_for_vp(doc, vp_name):
|
||||
@@ -776,7 +778,7 @@ def _rescale_doc_patterns(doc, factor):
|
||||
doc.Objects.Replace(obj.Id, g2)
|
||||
n_h += 1
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] hatch rescale:", ex)
|
||||
print("[SCALE] hatch rescale:", ex)
|
||||
# Per-Objekt Linetype-Scale (Rhino 8 Attribut)
|
||||
try:
|
||||
a = obj.Attributes
|
||||
@@ -784,7 +786,7 @@ def _rescale_doc_patterns(doc, factor):
|
||||
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
|
||||
# Nur Objekte mit explizit seter Skala anfassen
|
||||
# (Default=1.0 ueberlassen wir dem globalen Multiplikator).
|
||||
new_a = a.Duplicate()
|
||||
setattr(new_a, prop, cur * factor)
|
||||
@@ -794,7 +796,7 @@ def _rescale_doc_patterns(doc, factor):
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] _rescale_doc_patterns:", ex)
|
||||
print("[SCALE] _rescale_doc_patterns:", ex)
|
||||
|
||||
# Globale Linetype-Pattern-Length-Skala (Rhino-doc-Setting) versuchen.
|
||||
# Property-Namen variieren je nach Version — wir probieren.
|
||||
@@ -813,7 +815,7 @@ def _rescale_doc_patterns(doc, factor):
|
||||
pass
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
print("[MASSSTAB] Rescale x{:.4f}: {} Hatches, {} per-obj Linetypes{}".format(
|
||||
print("[SCALE] Rescale x{:.4f}: {} Hatches, {} per-obj Linetypes{}".format(
|
||||
factor, n_h, n_l, ", global Linetype-Scale" if set_global else ""))
|
||||
|
||||
|
||||
@@ -831,7 +833,7 @@ def _apply_scale(doc, vp, ratio):
|
||||
if vp is None or doc is None: return False
|
||||
try:
|
||||
if not vp.IsParallelProjection:
|
||||
print("[MASSSTAB] Viewport ist nicht parallel — Skala nicht setzbar")
|
||||
print("[SCALE] Viewport ist nicht parallel — Skala nicht setzbar")
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
@@ -854,7 +856,7 @@ def _apply_scale(doc, vp, ratio):
|
||||
# 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)
|
||||
print("[SCALE] _apply_scale: ungueltiger Faktor", factor)
|
||||
return False
|
||||
applied = False
|
||||
# Verschiedene API-Signaturen je nach Rhino-Version durchprobieren.
|
||||
@@ -873,7 +875,7 @@ def _apply_scale(doc, vp, ratio):
|
||||
Rhino.RhinoApp.RunScript("_-Zoom _Factor {:.6f} _Enter".format(factor), False)
|
||||
applied = True
|
||||
except Exception as ex3:
|
||||
print("[MASSSTAB] _apply_scale alle Varianten fehlgeschlagen:",
|
||||
print("[SCALE] _apply_scale alle Varianten failed:",
|
||||
ex1, ex2, ex3)
|
||||
if not applied:
|
||||
return False
|
||||
@@ -882,28 +884,37 @@ def _apply_scale(doc, vp, ratio):
|
||||
if _get_lineweights_enabled(doc):
|
||||
_apply_scaled_lineweights(doc, True, float(ratio))
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] LW-Rescale:", ex)
|
||||
print("[SCALE] 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).
|
||||
print("[SCALE] Hatch-Rescale:", ex)
|
||||
# Neuen Wert ZUERST persistieren — sowohl per-Viewport (fuer das
|
||||
# Dropdown, damit jeder Viewport seinen eigenen Massstab behaelt) als
|
||||
# auch als globaler "letzter Wert". WICHTIG: vor dem Raumstempel-
|
||||
# Regen weil _resolve_raum_text_height_m get_applied_scale_ratio()
|
||||
# liest — sonst regennt mit ALTER Skala.
|
||||
_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)
|
||||
print("[SCALE] per-vp scale write:", ex)
|
||||
# Raumstempel im masstab-Modus regennen mit der NEUEN Skala.
|
||||
try:
|
||||
import elemente as _el
|
||||
n_regen = _el.regen_masstab_raeume(doc)
|
||||
if n_regen > 0:
|
||||
print("[SCALE] {} masstab-Raum/Raeume regenned".format(n_regen))
|
||||
except Exception as ex:
|
||||
print("[SCALE] Raumstempel-Regen:", ex)
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
print("[MASSSTAB] Skala 1:{:.2f} gesetzt (Faktor {:.4f}, soll-frustum {:.4f} {})".format(
|
||||
print("[SCALE] Skala 1:{:.2f} set (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)
|
||||
print("[SCALE] _apply_scale:", ex)
|
||||
return False
|
||||
|
||||
|
||||
@@ -913,7 +924,7 @@ def _zoom_extents(doc, vp, selected_only=False):
|
||||
if selected_only:
|
||||
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||
if not objs:
|
||||
print("[MASSSTAB] Keine Selektion fuer Zoom-Selection")
|
||||
print("[SCALE] Keine Selektion fuer Zoom-Selection")
|
||||
return False
|
||||
bbox = Rhino.Geometry.BoundingBox.Empty
|
||||
for o in objs:
|
||||
@@ -946,7 +957,7 @@ def _zoom_extents(doc, vp, selected_only=False):
|
||||
except Exception: pass
|
||||
return True
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] _zoom_extents:", ex)
|
||||
print("[SCALE] _zoom_extents:", ex)
|
||||
return False
|
||||
|
||||
|
||||
@@ -961,7 +972,7 @@ class MassstabBridge(panel_base.BaseBridge):
|
||||
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)
|
||||
except Exception as ex: print("[SCALE] bootstrap:", ex)
|
||||
self._send_state(force=True)
|
||||
|
||||
def handle(self, data):
|
||||
@@ -1003,7 +1014,7 @@ class MassstabBridge(panel_base.BaseBridge):
|
||||
elif t == "DETECT_DPI":
|
||||
v = _force_redetect_dpi()
|
||||
if v is None:
|
||||
print("[MASSSTAB] Auto-Detect: keine Bildschirminfo verfuegbar")
|
||||
print("[SCALE] Auto-Detect: keine Bildschirminfo verfuegbar")
|
||||
self._send_state(force=True)
|
||||
elif t == "SET_LINEWEIGHTS":
|
||||
doc, _ = _active_vp()
|
||||
@@ -1050,7 +1061,7 @@ def _install_listeners(bridge):
|
||||
Rhino.RhinoApp.Idle += on_idle
|
||||
Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change
|
||||
sc.sticky[flag] = True
|
||||
print("[MASSSTAB] Listener aktiv (Idle-Poll + Doc-Change)")
|
||||
print("[SCALE] Listener active (Idle-Poll + Doc-Change)")
|
||||
|
||||
|
||||
def get_current_scale_ratio():
|
||||
@@ -1090,7 +1101,7 @@ def _bridge_factory():
|
||||
# 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"))
|
||||
panel_base.register_and_open("massstab", "Massstab", PANEL_GUID_STR, _bridge_factory,
|
||||
icon_spec=("straighten", "#c87050"))
|
||||
|
||||
# register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE
|
||||
|
||||
@@ -1,513 +0,0 @@
|
||||
# ! 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"))
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
OSM-Importer fuer Dossier — holt OpenStreetMap-Daten via Overpass-API als
|
||||
Polylinien (Strassen, Gebaeudeumrisse, Wasser, Gruenflaechen, Wege).
|
||||
|
||||
Pipeline:
|
||||
Adresse → bbox (LV95) → bbox (WGS84) → Overpass-Query →
|
||||
JSON-Response → OSM-Ways → Polylinien (in Doc-Units) → Rhino-Layer
|
||||
|
||||
Koord-Konversion WGS84↔LV95 nutzt swisstopo.wgs84_to_lv95 (LV95 ist die
|
||||
gemeinsame Basis mit dem swisstopo-Importer).
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
import Rhino
|
||||
import Rhino.Geometry as rg
|
||||
|
||||
import swisstopo # fuer wgs84_to_lv95
|
||||
|
||||
|
||||
OVERPASS_URL = "https://overpass-api.de/api/interpreter"
|
||||
|
||||
|
||||
# --- Kategorien ------------------------------------------------------------
|
||||
# Jede Kategorie liefert (Overpass-Selektor, Layer-Code, Layer-Name, Color).
|
||||
# Codes 7100-7199 reserviert fuer OSM-Sub-Ebenen unter '70_osm'.
|
||||
CATEGORIES = {
|
||||
"streets": {
|
||||
"selector": '[highway~"^(motorway|trunk|primary|secondary|tertiary|residential|unclassified|service|living_street|pedestrian)$"]',
|
||||
"code": "7101", "name": "Strassen", "color": "#a89070",
|
||||
},
|
||||
"buildings": {
|
||||
"selector": '[building]',
|
||||
"code": "7102", "name": "Gebaeudeumrisse", "color": "#888888",
|
||||
"include_relations": True,
|
||||
},
|
||||
"water": {
|
||||
"selector": '[natural=water]',
|
||||
"code": "7103", "name": "Wasser", "color": "#4080a0",
|
||||
"include_relations": True,
|
||||
},
|
||||
"waterways": {
|
||||
"selector": '[waterway~"^(river|stream|canal)$"]',
|
||||
"code": "7104", "name": "Wasserlaeufe", "color": "#4080a0",
|
||||
},
|
||||
"parks": {
|
||||
"selector": '[leisure~"^(park|garden)$"]',
|
||||
"code": "7105", "name": "Parks", "color": "#60a070",
|
||||
"include_relations": True,
|
||||
},
|
||||
"forest": {
|
||||
"selector": '[landuse~"^(forest|grass|meadow)$"]',
|
||||
"code": "7106", "name": "Wald_Gruen", "color": "#406050",
|
||||
"include_relations": True,
|
||||
},
|
||||
"footpaths": {
|
||||
"selector": '[highway~"^(footway|path|track|cycleway)$"]',
|
||||
"code": "7107", "name": "Wege", "color": "#806040",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_overpass_query(bbox_wgs, categories):
|
||||
"""Baut die Overpass-QL-Query fuer bbox + ausgewaehlte Kategorien.
|
||||
bbox_wgs: (min_lon, min_lat, max_lon, max_lat) — WGS84."""
|
||||
south = bbox_wgs[1]; west = bbox_wgs[0]
|
||||
north = bbox_wgs[3]; east = bbox_wgs[2]
|
||||
bbox_str = "{},{},{},{}".format(south, west, north, east)
|
||||
parts = []
|
||||
for cat in categories:
|
||||
spec = CATEGORIES.get(cat)
|
||||
if not spec: continue
|
||||
parts.append('way{}({});'.format(spec["selector"], bbox_str))
|
||||
if spec.get("include_relations"):
|
||||
parts.append('relation{}({});'.format(spec["selector"], bbox_str))
|
||||
body = ''.join(parts)
|
||||
return '[out:json][timeout:60];({});out body;>;out skel qt;'.format(body)
|
||||
|
||||
|
||||
def fetch_overpass(bbox_wgs, categories, progress=None):
|
||||
"""Schickt Overpass-Query, liefert JSON-Dict oder None."""
|
||||
q = build_overpass_query(bbox_wgs, categories)
|
||||
if progress: progress("Overpass-Query ({} Kategorien)...".format(len(categories)))
|
||||
data = urllib.parse.urlencode({"data": q}).encode("utf-8")
|
||||
req = urllib.request.Request(OVERPASS_URL, data=data, method="POST",
|
||||
headers={"User-Agent": "Dossier/OSM-Importer"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180) as resp:
|
||||
text = resp.read().decode("utf-8", errors="ignore")
|
||||
out = json.loads(text)
|
||||
if progress: progress("Antwort: {} Elemente".format(len(out.get("elements", []))))
|
||||
return out
|
||||
except Exception as ex:
|
||||
if progress: progress("Overpass fail: {}".format(ex))
|
||||
return None
|
||||
|
||||
|
||||
def parse_osm_elements(osm_json):
|
||||
"""Zerlegt OSM-JSON in {nodes: {id: (lon, lat)}, ways: [{id, nodes, tags}]}."""
|
||||
if not osm_json: return None
|
||||
nodes = {}
|
||||
ways = []
|
||||
for el in osm_json.get("elements", []):
|
||||
t = el.get("type")
|
||||
if t == "node":
|
||||
nodes[el["id"]] = (el["lon"], el["lat"])
|
||||
elif t == "way":
|
||||
ways.append({
|
||||
"id": el["id"],
|
||||
"nodes": el.get("nodes", []),
|
||||
"tags": el.get("tags") or {},
|
||||
})
|
||||
return {"nodes": nodes, "ways": ways}
|
||||
|
||||
|
||||
def classify_way(tags):
|
||||
"""Mappt Way-Tags auf eine Kategorie-Key (oder None falls uninteressant)."""
|
||||
if not tags: return None
|
||||
hw = tags.get("highway")
|
||||
if hw in ("motorway","trunk","primary","secondary","tertiary",
|
||||
"residential","unclassified","service","living_street","pedestrian"):
|
||||
return "streets"
|
||||
if hw in ("footway","path","track","cycleway"): return "footpaths"
|
||||
if tags.get("building"): return "buildings"
|
||||
if tags.get("natural") == "water": return "water"
|
||||
ww = tags.get("waterway")
|
||||
if ww in ("river","stream","canal"): return "waterways"
|
||||
if tags.get("leisure") in ("park","garden"): return "parks"
|
||||
if tags.get("landuse") in ("forest","grass","meadow"): return "forest"
|
||||
return None
|
||||
|
||||
|
||||
def way_to_polyline(way_node_ids, nodes, shift_lv95, m_to_unit, z=0.0):
|
||||
"""OSM-Way → Rhino.Polyline in Doc-Units. shift_lv95 = (sx, sy, sz) Origin-
|
||||
Shift in LV95-Metern (gleicher Pipeline wie swisstopo)."""
|
||||
pts = []
|
||||
sx, sy, sz = shift_lv95
|
||||
for nid in way_node_ids:
|
||||
node = nodes.get(nid)
|
||||
if node is None: continue
|
||||
lon, lat = node
|
||||
e, n = swisstopo.wgs84_to_lv95(lon, lat)
|
||||
x = (e - sx) * m_to_unit
|
||||
y = (n - sy) * m_to_unit
|
||||
pts.append(rg.Point3d(x, y, z))
|
||||
if len(pts) < 2: return None
|
||||
poly = rg.Polyline(pts)
|
||||
return poly
|
||||
|
||||
|
||||
def import_osm_to_doc(doc, bbox_wgs, categories, shift_lv95, m_to_unit,
|
||||
z_doc=0.0, progress=None):
|
||||
"""End-to-end-Import: Overpass-Query + Polylinien-Erzeugung. Liefert
|
||||
Liste von dicts: [{category, obj_id, way_tags}, ...] — Aufrufer macht
|
||||
Layer-Move + Tag selbst."""
|
||||
osm_json = fetch_overpass(bbox_wgs, categories, progress=progress)
|
||||
if osm_json is None: return []
|
||||
parsed = parse_osm_elements(osm_json)
|
||||
if not parsed: return []
|
||||
nodes = parsed["nodes"]
|
||||
ways = parsed["ways"]
|
||||
if progress: progress("Parse {} Ways...".format(len(ways)))
|
||||
created = []
|
||||
for way in ways:
|
||||
cat = classify_way(way["tags"])
|
||||
if cat is None or cat not in categories: continue
|
||||
poly = way_to_polyline(way["nodes"], nodes, shift_lv95,
|
||||
m_to_unit, z=z_doc)
|
||||
if poly is None or poly.Count < 2: continue
|
||||
# Wenn Polyline geschlossen ist (erster == letzter Punkt) → als Curve
|
||||
# mit Schluss-Edge, sonst offene Polyline.
|
||||
curve = poly.ToNurbsCurve()
|
||||
if curve is None: continue
|
||||
gid = doc.Objects.AddCurve(curve)
|
||||
if gid is None: continue
|
||||
obj = doc.Objects.Find(gid)
|
||||
if obj is None: continue
|
||||
created.append({
|
||||
"category": cat,
|
||||
"obj": obj,
|
||||
"tags": way["tags"],
|
||||
})
|
||||
if progress: progress("→ {} OSM-Linien erzeugt".format(len(created)))
|
||||
return created
|
||||
+156
-6
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
overrides.py
|
||||
Engine fuer regelbasierte grafische Overrides (ArchiCAD Graphical Overrides /
|
||||
@@ -54,6 +56,9 @@ _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")
|
||||
# Rule-Templates: einzelne wiederverwendbare Regeln (cross-doc). Andere
|
||||
# Datei damit User Combo-Presets und Einzel-Templates separat verwalten kann.
|
||||
_RULE_TPL_PATH = os.path.join(_PRESETS_DIR, "override_rule_templates.json")
|
||||
|
||||
# UserString-Keys fuer Original-Backups (pro Objekt)
|
||||
_ORIG_COLOR_SRC = "dossier_or_csrc"
|
||||
@@ -70,6 +75,9 @@ _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
|
||||
_ORIG_HC_SRC = "dossier_or_hatch_csrc" # auf Hatch — original ColorSource
|
||||
_ORIG_HC = "dossier_or_hatch_color" # auf Hatch — original Color
|
||||
_HATCH_COLOR_OVERRIDDEN = "dossier_or_hatch_color_done"
|
||||
|
||||
_FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer
|
||||
_FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject
|
||||
@@ -180,11 +188,77 @@ def delete_preset(name):
|
||||
return _write_presets_file(new)
|
||||
|
||||
|
||||
# --- Rule-Templates: einzelne wiederverwendbare Regeln (cross-doc) ----------
|
||||
|
||||
def _read_rule_templates():
|
||||
if not os.path.isfile(_RULE_TPL_PATH): return []
|
||||
try:
|
||||
with open(_RULE_TPL_PATH, "rb") as f:
|
||||
data = json.loads(f.read().decode("utf-8"))
|
||||
if isinstance(data, list): return data
|
||||
if isinstance(data, dict) and "templates" in data:
|
||||
return data.get("templates") or []
|
||||
except Exception as ex:
|
||||
print("[OVERRIDES] read_rule_templates:", ex)
|
||||
return []
|
||||
|
||||
|
||||
def _write_rule_templates(templates):
|
||||
try:
|
||||
if not os.path.isdir(_PRESETS_DIR):
|
||||
os.makedirs(_PRESETS_DIR)
|
||||
with open(_RULE_TPL_PATH, "wb") as f:
|
||||
f.write(json.dumps(templates or [], ensure_ascii=False, indent=2).encode("utf-8"))
|
||||
return True
|
||||
except Exception as ex:
|
||||
print("[OVERRIDES] write_rule_templates:", ex)
|
||||
return False
|
||||
|
||||
|
||||
def list_rule_templates():
|
||||
"""Liefert Liste von {name, rule} fuer alle gespeicherten Templates."""
|
||||
out = []
|
||||
for t in _read_rule_templates():
|
||||
if not isinstance(t, dict): continue
|
||||
out.append({"name": t.get("name", "(ohne Name)"),
|
||||
"rule": t.get("rule") or {}})
|
||||
return out
|
||||
|
||||
|
||||
def save_rule_template(name, rule):
|
||||
"""Speichert/ueberschreibt eine Regel als Template unter name."""
|
||||
if not name or not isinstance(name, str): return False
|
||||
name = name.strip()
|
||||
if not name or not isinstance(rule, dict): return False
|
||||
templates = _read_rule_templates()
|
||||
for i, t in enumerate(templates):
|
||||
if isinstance(t, dict) and t.get("name") == name:
|
||||
templates[i] = {"name": name, "rule": rule}
|
||||
return _write_rule_templates(templates)
|
||||
templates.append({"name": name, "rule": rule})
|
||||
return _write_rule_templates(templates)
|
||||
|
||||
|
||||
def load_rule_template(name):
|
||||
"""Liefert die Rule eines Templates oder None."""
|
||||
for t in _read_rule_templates():
|
||||
if isinstance(t, dict) and t.get("name") == name:
|
||||
return json.loads(json.dumps(t.get("rule") or {}))
|
||||
return None
|
||||
|
||||
|
||||
def delete_rule_template(name):
|
||||
templates = _read_rule_templates()
|
||||
new = [t for t in templates if not (isinstance(t, dict) and t.get("name") == name)]
|
||||
if len(new) == len(templates): return False
|
||||
return _write_rule_templates(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."""
|
||||
geclear-t, Rules bleiben unchanged (User waehlt "kein Preset"). Bei
|
||||
aktivem enabled-Flag wird sofort neu applied. True bei Erfolg."""
|
||||
if doc is None: return False
|
||||
cfg = load_config(doc)
|
||||
if name:
|
||||
@@ -342,6 +416,7 @@ def _restore_original(doc, obj):
|
||||
# Hatch separat zuruecksetzen — kann auch ohne Curve-Override
|
||||
# passiert sein (z.B. wenn Override nur den Pattern aendert)
|
||||
_restore_hatch(doc, obj)
|
||||
_restore_hatch_color(doc, obj)
|
||||
if a.GetUserString(_OVERRIDDEN) != "1":
|
||||
return False
|
||||
try:
|
||||
@@ -481,6 +556,74 @@ def _restore_hatch(doc, curve_obj):
|
||||
return False
|
||||
|
||||
|
||||
def _apply_hatch_color_override(doc, curve_obj, color_hex):
|
||||
"""Setzt ObjectColor + ColorSource des verlinkten Hatches auf color_hex.
|
||||
Backup wird einmalig auf dem Hatch in UserStrings gesichert."""
|
||||
h = _find_linked_hatch(doc, curve_obj)
|
||||
if h is None: return False
|
||||
try:
|
||||
ha = h.Attributes
|
||||
if ha.GetUserString(_HATCH_COLOR_OVERRIDDEN) != "1":
|
||||
try:
|
||||
ha.SetUserString(_ORIG_HC_SRC, str(int(ha.ColorSource)))
|
||||
ha.SetUserString(_ORIG_HC, _color_to_hex(ha.ObjectColor))
|
||||
ha.SetUserString(_HATCH_COLOR_OVERRIDDEN, "1")
|
||||
doc.Objects.ModifyAttributes(h, ha, True)
|
||||
except Exception as ex:
|
||||
print("[OVERRIDES] hatch-color backup:", ex)
|
||||
new_a = h.Attributes.Duplicate()
|
||||
new_a.ColorSource = _FROM_OBJECT
|
||||
new_a.ObjectColor = _hex_to_color(color_hex)
|
||||
try:
|
||||
new_a.PlotColorSource = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject
|
||||
new_a.PlotColor = new_a.ObjectColor
|
||||
except Exception: pass
|
||||
doc.Objects.ModifyAttributes(h, new_a, True)
|
||||
return True
|
||||
except Exception as ex:
|
||||
print("[OVERRIDES] _apply_hatch_color_override:", ex)
|
||||
return False
|
||||
|
||||
|
||||
def _restore_hatch_color(doc, curve_obj):
|
||||
"""Stellt ColorSource + ObjectColor des verlinkten Hatches aus Backup
|
||||
wieder her."""
|
||||
h = _find_linked_hatch(doc, curve_obj)
|
||||
if h is None: return False
|
||||
a = h.Attributes
|
||||
if a.GetUserString(_HATCH_COLOR_OVERRIDDEN) != "1": return False
|
||||
try:
|
||||
orig_src = a.GetUserString(_ORIG_HC_SRC) or "1" # default ColorFromObject
|
||||
orig_col = a.GetUserString(_ORIG_HC) or "#f5f5f5"
|
||||
new_a = h.Attributes.Duplicate()
|
||||
# ColorSource zuruecksetzen — Enum.ToObject ist in IronPython3
|
||||
# zuverlaessiger als der direkte int->Enum-Konstruktor.
|
||||
try:
|
||||
val = int(orig_src)
|
||||
new_a.ColorSource = System.Enum.ToObject(
|
||||
Rhino.DocObjects.ObjectColorSource, val)
|
||||
except Exception:
|
||||
new_a.ColorSource = _FROM_OBJECT
|
||||
try:
|
||||
new_a.ObjectColor = _hex_to_color(orig_col)
|
||||
except Exception:
|
||||
new_a.ObjectColor = Drawing.Color.FromArgb(245, 245, 245)
|
||||
# PlotColor mit-resetten
|
||||
try:
|
||||
new_a.PlotColorSource = (
|
||||
Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject)
|
||||
new_a.PlotColor = new_a.ObjectColor
|
||||
except Exception: pass
|
||||
for k in (_ORIG_HC_SRC, _ORIG_HC, _HATCH_COLOR_OVERRIDDEN):
|
||||
try: new_a.SetUserString(k, "")
|
||||
except Exception: pass
|
||||
doc.Objects.ModifyAttributes(h, new_a, True)
|
||||
return True
|
||||
except Exception as ex:
|
||||
print("[OVERRIDES] _restore_hatch_color:", ex)
|
||||
return False
|
||||
|
||||
|
||||
def _apply_to_object(doc, obj, overrides):
|
||||
"""Setzt die Override-Werte am Objekt. Sichert vorher Originale."""
|
||||
if not overrides: return False
|
||||
@@ -498,6 +641,11 @@ def _apply_to_object(doc, obj, overrides):
|
||||
new_a.PlotColor = col
|
||||
except Exception: pass
|
||||
changed = True
|
||||
# Verlinkten Hatch (Gestaltung-Fuellung) auch einfaerben — sonst
|
||||
# bleibt die Fuellung in der Original-Farbe waehrend die Outline schon
|
||||
# die Override-Farbe traegt.
|
||||
try: _apply_hatch_color_override(doc, obj, overrides["color"])
|
||||
except Exception: pass
|
||||
if "lineweight" in overrides:
|
||||
try:
|
||||
new_a.PlotWeightSource = _LW_FROM_OBJ
|
||||
@@ -528,7 +676,7 @@ def _apply_to_object(doc, obj, overrides):
|
||||
|
||||
def apply_all(doc):
|
||||
"""Wendet alle aktiven Regeln auf alle Objekte im Doc an.
|
||||
Objekte die NICHT (mehr) matchen werden auf Originale zurueckgesetzt."""
|
||||
Objekte die NICHT (mehr) matchen werden auf Originale zurueckset."""
|
||||
if doc is None: return 0, 0
|
||||
cfg = load_config(doc)
|
||||
if not cfg.get("enabled"): return 0, 0
|
||||
@@ -574,8 +722,10 @@ def restore_all(doc):
|
||||
if _restore_original(doc, obj):
|
||||
n += 1
|
||||
else:
|
||||
# Vielleicht nur Hatch-Override
|
||||
if _restore_hatch(doc, obj):
|
||||
# Vielleicht nur Hatch-Override (Pattern und/oder Color)
|
||||
r1 = _restore_hatch(doc, obj)
|
||||
r2 = _restore_hatch_color(doc, obj)
|
||||
if r1 or r2:
|
||||
n += 1
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
@@ -715,4 +865,4 @@ def install_listeners():
|
||||
return
|
||||
|
||||
sc.sticky["overrides_listeners"] = True
|
||||
print("[OVERRIDES] Live-Update Listener aktiv (Add/Replace/LayerTable)")
|
||||
print("[OVERRIDES] Live-Update Listener active (Add/Replace/LayerTable)")
|
||||
|
||||
+64
-10
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
overrides_panel.py
|
||||
OVERRIDES-Panel: Rule-Editor fuer grafische Overrides.
|
||||
@@ -74,6 +76,7 @@ def _payload(doc):
|
||||
"hatchPatterns": _list_hatch_patterns(doc),
|
||||
"presets": overrides.list_presets(),
|
||||
"activePreset": cfg.get("activePreset"),
|
||||
"ruleTemplates": overrides.list_rule_templates(),
|
||||
}
|
||||
|
||||
|
||||
@@ -196,7 +199,7 @@ class OverridesBridge(panel_base.BaseBridge):
|
||||
overrides.set_active_preset(doc, name)
|
||||
else:
|
||||
# Append-Mode: bestehende + Preset-Rules. activePreset wird
|
||||
# in update_rules auf None gesetzt — passt, weil's eine
|
||||
# in update_rules auf None set — passt, weil's eine
|
||||
# Mischung ist, kein einzelnes Preset mehr.
|
||||
rules = overrides.load_preset(name)
|
||||
if rules is not None:
|
||||
@@ -210,17 +213,68 @@ class OverridesBridge(panel_base.BaseBridge):
|
||||
overrides.delete_preset(name)
|
||||
self._send_state()
|
||||
|
||||
# --- Rule-Templates (cross-doc, einzelne Regeln) ----------------
|
||||
elif t == "SAVE_RULE_TEMPLATE":
|
||||
name = (p.get("name") or "").strip()
|
||||
rule = p.get("rule") or {}
|
||||
if name and isinstance(rule, dict):
|
||||
# ID/Name aus dem Template-Rule herausschneiden (template hat
|
||||
# eigenen Namen, ID wird beim Insert neu generiert)
|
||||
clean = {k: v for k, v in rule.items() if k not in ("id",)}
|
||||
overrides.save_rule_template(name, clean)
|
||||
self._send_state()
|
||||
elif t == "DELETE_RULE_TEMPLATE":
|
||||
name = (p.get("name") or "").strip()
|
||||
if name:
|
||||
overrides.delete_rule_template(name)
|
||||
self._send_state()
|
||||
elif t == "ADD_FROM_TEMPLATE":
|
||||
name = (p.get("name") or "").strip()
|
||||
if not name: return
|
||||
tpl = overrides.load_rule_template(name)
|
||||
if not tpl: return
|
||||
cfg = overrides.load_config(doc)
|
||||
new_rule = dict(tpl)
|
||||
new_rule["id"] = "rule_" + uuid.uuid4().hex[:8]
|
||||
new_rule.setdefault("enabled", True)
|
||||
new_rule.setdefault("name", name)
|
||||
rules = cfg.get("rules") or []
|
||||
rules.insert(0, new_rule)
|
||||
overrides.update_rules(doc, rules, cfg.get("enabled"))
|
||||
self._send_state()
|
||||
|
||||
def _bridge_factory():
|
||||
|
||||
def _ensure_listeners_once():
|
||||
"""Overrides-Listener nur EINMAL global installieren (statt bei jedem
|
||||
open_as_window)."""
|
||||
if sc.sticky.get("overrides_listeners_installed"):
|
||||
return
|
||||
try:
|
||||
overrides.install_listeners()
|
||||
sc.sticky["overrides_listeners_installed"] = True
|
||||
except Exception as ex:
|
||||
print("[OVERRIDES] install_listeners:", ex)
|
||||
|
||||
|
||||
def open_as_window():
|
||||
"""Oeffnet OVERRIDES als echtes Rhino-Fenster (Eto.Form + WebView).
|
||||
Wird vom Oberleiste-Bridge bei OPEN_OVERRIDES_PANEL gerufen.
|
||||
|
||||
Pro Fenster eine eigene OverridesBridge-Instanz. Die letzte Instanz
|
||||
landet in sticky["overrides_bridge"] — andere Panels (Oberleiste) die
|
||||
Cross-Updates an Overrides senden, treffen das aktive Fenster."""
|
||||
_ensure_listeners_once()
|
||||
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.open_satellite_window(
|
||||
"overrides",
|
||||
title="Overrides",
|
||||
size=(760, 580),
|
||||
bridge=b)
|
||||
|
||||
|
||||
panel_base.register_and_open("overrides", "OVERRIDES", PANEL_GUID_STR, _bridge_factory,
|
||||
icon_spec=("V", "#b5621e"),
|
||||
min_size=(720, 560))
|
||||
# OVERRIDES laeuft jetzt als Satelliten-Fenster (geoeffnet vom Oberleiste-
|
||||
# Gear-Button), nicht mehr als gedocktes Panel. Listener werden lazy beim
|
||||
# ersten open_as_window installiert. Falls jemand das alte Panel via
|
||||
# Workspace-Layout noch geoeffnet hat, wird es nicht mehr registriert →
|
||||
# Rhino zeigt es leer / nicht mehr an.
|
||||
|
||||
+523
-58
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
panel_base.py
|
||||
Geteilte Infrastruktur fuer dockbare Rhino-Panels mit React-WebView.
|
||||
@@ -8,6 +10,7 @@ Wird von rhinopanel.py (EBENEN) und gestaltung.py (GESTALTUNG) verwendet.
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import Rhino
|
||||
import Rhino.UI as RhinoUI
|
||||
import Eto.Forms as forms
|
||||
@@ -17,6 +20,82 @@ import scriptcontext as sc
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_DIST = os.path.join(_HERE, "..", "dist", "index.html")
|
||||
_SETTINGS_PATH = os.path.expanduser(
|
||||
"~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json")
|
||||
|
||||
|
||||
_MODE_LOG_TAG = {
|
||||
"ebenen": "LAYERS",
|
||||
"zeichnungsebenen": "DRAWING-LEVELS",
|
||||
"oberleiste": "TOOLBAR",
|
||||
"gestaltung": "STYLES",
|
||||
"werkzeuge": "TOOLS",
|
||||
"dimensionen": "DIMENSIONS",
|
||||
"ausschnitte": "VIEWPORTS",
|
||||
"massstab": "SCALE",
|
||||
"overrides": "OVERRIDES",
|
||||
"layouts": "LAYOUTS",
|
||||
"elemente": "ELEMENTS",
|
||||
"kamera": "CAMERA",
|
||||
"layer_combinations": "LAYER-COMBINATIONS",
|
||||
"dossier_settings": "DOSSIER-SETTINGS",
|
||||
"project_settings": "PROJECT-SETTINGS",
|
||||
}
|
||||
|
||||
def _tag(mode):
|
||||
return _MODE_LOG_TAG.get(mode, mode.upper())
|
||||
|
||||
|
||||
def _read_lang():
|
||||
"""Liest die UI-Sprache aus dossier_settings.json. Default: 'de'."""
|
||||
try:
|
||||
if os.path.isfile(_SETTINGS_PATH):
|
||||
with open(_SETTINGS_PATH, "rb") as f:
|
||||
d = json.loads(f.read().decode("utf-8"))
|
||||
lang = d.get("lang", "de")
|
||||
return lang if lang in ("de", "en") else "de"
|
||||
except Exception:
|
||||
pass
|
||||
return "de"
|
||||
|
||||
|
||||
# --- Timing-Instrumentierung ------------------------------------------------
|
||||
# Schaltbar via sc.sticky["dossier_timing"] = True. Wir loggen die Hot-Paths
|
||||
# beim Plugin-Start. Tabelle am Ende per panel_base.print_startup_summary().
|
||||
_TIMINGS = [] # Liste von (phase, label, ms)
|
||||
_T0 = None # Zeitstempel des allerersten Aufrufs
|
||||
|
||||
def _t_mark(phase, label, t_start):
|
||||
"""Loggt eine Phase + Dauer. Print sofort, plus Aggregat fuers Summary."""
|
||||
ms = (time.time() - t_start) * 1000.0
|
||||
_TIMINGS.append((phase, label, ms))
|
||||
global _T0
|
||||
if _T0 is None: _T0 = t_start
|
||||
try:
|
||||
print("[STARTUP] {:>22s} {:>22s} {:7.1f} ms".format(phase, label, ms))
|
||||
except Exception: pass
|
||||
return ms
|
||||
|
||||
def print_startup_summary():
|
||||
"""Aufruf am Ende von startup.py: zeigt total + sortierte Topliste."""
|
||||
if not _TIMINGS: return
|
||||
total_wall = (time.time() - (_T0 or time.time())) * 1000.0
|
||||
total_work = sum(ms for _, _, ms in _TIMINGS)
|
||||
print("[STARTUP] ===== SUMMARY =====")
|
||||
print("[STARTUP] Wall-time (first to last mark): {:.1f} ms".format(total_wall))
|
||||
print("[STARTUP] Sum of measured work: {:.1f} ms".format(total_work))
|
||||
# Top-10 by duration
|
||||
top = sorted(_TIMINGS, key=lambda x: -x[2])[:10]
|
||||
print("[STARTUP] --- Top-10 by duration ---")
|
||||
for phase, label, ms in top:
|
||||
print("[STARTUP] {:7.1f} ms {} / {}".format(ms, phase, label))
|
||||
# Aggregate by phase
|
||||
by_phase = {}
|
||||
for phase, _, ms in _TIMINGS:
|
||||
by_phase[phase] = by_phase.get(phase, 0.0) + ms
|
||||
print("[STARTUP] --- Aggregate by phase ---")
|
||||
for phase, ms in sorted(by_phase.items(), key=lambda x: -x[1]):
|
||||
print("[STARTUP] {:7.1f} ms {}".format(ms, phase))
|
||||
|
||||
|
||||
# --- Legacy-Migration: traite_* / pause_* -> dossier_* ----------------------
|
||||
@@ -56,7 +135,7 @@ def migrate_to_dossier(doc):
|
||||
new = "dossier_" + suffix
|
||||
try:
|
||||
if doc.Strings.GetValue(new):
|
||||
continue # Dossier-Variante vorhanden -> nicht ueberschreiben
|
||||
continue # Dossier-Variante present -> nicht ueberschreiben
|
||||
for prefix in _LEGACY_PREFIXES:
|
||||
old_v = doc.Strings.GetValue(prefix + suffix)
|
||||
if old_v:
|
||||
@@ -150,7 +229,7 @@ class BaseBridge(object):
|
||||
try:
|
||||
data = json.loads(raw_str)
|
||||
except Exception as ex:
|
||||
print("[{}] JSON-Fehler: {}".format(self._mode.upper(), ex))
|
||||
print("[{}] JSON-Fehler: {}".format(_tag(self._mode), ex))
|
||||
return
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
@@ -166,8 +245,8 @@ class BaseBridge(object):
|
||||
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()))
|
||||
print("[{}] Chunk-Reassembly: {}".format(_tag(self._mode), ex))
|
||||
print("[{}] Traceback:\n{}".format(_tag(self._mode), traceback.format_exc()))
|
||||
else:
|
||||
self.handle(data)
|
||||
|
||||
@@ -198,20 +277,39 @@ class BaseBridge(object):
|
||||
|
||||
# --- HTML laden -------------------------------------------------------------
|
||||
|
||||
def load_inline(wv, mode):
|
||||
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE."""
|
||||
# Cache der fertig zusammengebauten Inline-HTML — Disk-IO + CSS/JS-String-
|
||||
# Inlining laeuft nur EINMAL pro Plugin-Session statt pro Panel-Mount. Bei
|
||||
# 10 Panels eliminiert das 9x ~395 KB Disk-Read + 9x Regex/Concat-Pass.
|
||||
# Cache-Key = mtime von dist/index.html — wenn der User neu build, wird
|
||||
# der Cache automatisch invalidiert.
|
||||
_INLINE_TEMPLATE = None # (mtime_signature, head_html, body_html_template)
|
||||
# Sentinel-Marker im Template fuer die per-Mount unterschiedlichen Scripts.
|
||||
# Wir nutzen einen "Magic-String" statt format/{} damit die JS-Bundle-Inhalte
|
||||
# (die {} Token enthalten koennen) nicht versehentlich matchen.
|
||||
_MODE_SCRIPT_PLACEHOLDER = "/*__DOSSIER_MODE_SCRIPT__*/"
|
||||
|
||||
|
||||
def _build_inline_template():
|
||||
"""Liest dist/index.html + inline alle CSS/JS. Liefert die HTML-String
|
||||
mit Placeholder fuer den per-Mount Mode-Script-Block."""
|
||||
t0 = time.time()
|
||||
if not os.path.exists(_DIST):
|
||||
print("[{}] dist nicht gefunden".format(mode.upper()))
|
||||
return
|
||||
return None, None
|
||||
dist_dir = os.path.dirname(_DIST)
|
||||
try:
|
||||
mtime_sig = os.path.getmtime(_DIST)
|
||||
except Exception:
|
||||
mtime_sig = 0
|
||||
with open(_DIST, "rb") as f:
|
||||
html = f.read().decode("utf-8")
|
||||
|
||||
mode_script = '<script>window.PANEL_MODE="{}";</script>'.format(mode)
|
||||
placeholder_script = '<script>' + _MODE_SCRIPT_PLACEHOLDER + '</script>'
|
||||
_no_select = '<style>*{-webkit-user-select:none!important;user-select:none!important;}</style>'
|
||||
_no_ctx = '<script>document.addEventListener("contextmenu",function(e){e.preventDefault();},true);</script>'
|
||||
if "</head>" in html:
|
||||
html = html.replace("</head>", mode_script + "</head>")
|
||||
html = html.replace("</head>", placeholder_script + _no_select + _no_ctx + "</head>")
|
||||
else:
|
||||
html = mode_script + html
|
||||
html = placeholder_script + _no_select + _no_ctx + html
|
||||
|
||||
def inline_css(m):
|
||||
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
|
||||
@@ -227,7 +325,48 @@ def load_inline(wv, mode):
|
||||
|
||||
html = re.sub(r'<link[^>]+href="(\./assets/[^"]+\.css)"[^>]*/?>', inline_css, html)
|
||||
html = re.sub(r'<script[^>]+src="(\./assets/[^"]+\.js)"[^>]*></script>', inline_js, html)
|
||||
_t_mark("html-build", "build_inline_template", t0)
|
||||
return mtime_sig, html
|
||||
|
||||
|
||||
def load_inline(wv, mode, params=None):
|
||||
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE.
|
||||
|
||||
`params` (optional dict): wird als `window.PANEL_PARAMS` injiziert. Wird
|
||||
von Satelliten-Fenstern (z.B. Settings-Dialoge) verwendet um initial-
|
||||
State an die React-App zu uebergeben.
|
||||
|
||||
Performance: das fertige Inline-HTML (mit allen CSS/JS embedded) wird
|
||||
modul-weit gecached. Pro Aufruf nur noch Mode-Script-Substitute + die
|
||||
teure WebView.LoadHtml — Disk-IO laeuft 1x pro Plugin-Session."""
|
||||
t0 = time.time()
|
||||
global _INLINE_TEMPLATE
|
||||
# Cache-Refresh wenn dist/index.html sich geaendert hat (neuer Build)
|
||||
try:
|
||||
cur_mtime = os.path.getmtime(_DIST)
|
||||
except Exception:
|
||||
cur_mtime = 0
|
||||
if _INLINE_TEMPLATE is None or _INLINE_TEMPLATE[0] != cur_mtime:
|
||||
sig, tmpl = _build_inline_template()
|
||||
if tmpl is None:
|
||||
print("[{}] dist not found".format(_tag(mode)))
|
||||
return
|
||||
_INLINE_TEMPLATE = (sig, tmpl)
|
||||
|
||||
# Per-Mount: nur das Mode-Script-Snippet bauen
|
||||
parts = ['window.PANEL_MODE="{}";'.format(mode),
|
||||
'window.DOSSIER_LANG="{}";'.format(_read_lang())]
|
||||
if params is not None:
|
||||
try:
|
||||
parts.append('window.PANEL_PARAMS=' + json.dumps(params, ensure_ascii=False) + ';')
|
||||
except Exception as ex:
|
||||
print("[{}] PANEL_PARAMS serialize: {}".format(_tag(mode), ex))
|
||||
mode_script = ''.join(parts)
|
||||
html = _INLINE_TEMPLATE[1].replace(_MODE_SCRIPT_PLACEHOLDER, mode_script)
|
||||
t_loadhtml = time.time()
|
||||
wv.LoadHtml(html)
|
||||
_t_mark("load_inline", mode, t0)
|
||||
_t_mark("LoadHtml", mode, t_loadhtml)
|
||||
|
||||
|
||||
def attach_webview(panel, bridge, mode):
|
||||
@@ -242,10 +381,10 @@ def attach_webview(panel, bridge, mode):
|
||||
try:
|
||||
bridge.handle_raw(title[10:])
|
||||
except Exception as ex:
|
||||
print("[{}] Message-Fehler: {}".format(mode.upper(), ex))
|
||||
print("[{}] Message-Fehler: {}".format(_tag(mode), ex))
|
||||
finally:
|
||||
try:
|
||||
wv.ExecuteScript("document.title='{}';".format(mode.upper()))
|
||||
wv.ExecuteScript("document.title='{}';".format(_tag(mode)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -254,19 +393,125 @@ def attach_webview(panel, bridge, mode):
|
||||
wv.ExecuteScript("window.RHINO_MODE=true;")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
wv.ExecuteScript(
|
||||
"var _ds=document.createElement('style');"
|
||||
"_ds.textContent='*{-webkit-user-select:none!important;user-select:none!important;}';"
|
||||
"document.head.appendChild(_ds);"
|
||||
"document.addEventListener('contextmenu',function(e){e.preventDefault();},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))
|
||||
print("[{}] Inline-Fehler: {}".format(_tag(mode), ex))
|
||||
|
||||
wv.DocumentTitleChanged += on_title
|
||||
wv.DocumentLoaded += on_loaded
|
||||
Rhino.RhinoApp.Idle += on_idle
|
||||
|
||||
|
||||
# --- Satelliten-Fenster (echtes Rhino-Fenster mit eingebetteter WebView) ----
|
||||
|
||||
def open_satellite_window(mode, params=None, title=None, size=(420, 560),
|
||||
on_save=None, on_cancel=None, bridge=None,
|
||||
topmost=False):
|
||||
"""Oeffnet ein echtes Rhino-Fenster (Eto.Form) mit eingebetteter WebView.
|
||||
Die WebView laedt die React-App mit dem gegebenen `mode` und `params`.
|
||||
|
||||
Zwei Bridge-Modi:
|
||||
- **Default (kein `bridge`-Arg):** inline SAVE/CANCEL-Bridge. React
|
||||
sendet SAVE/CANCEL → on_save/on_cancel-Callback → Fenster zu. Fuer
|
||||
einfache One-Shot-Dialoge (Settings etc.).
|
||||
- **`bridge` uebergeben:** eine vollwertige BaseBridge-Subklasse (z.B.
|
||||
OverridesBridge). Das Fenster nutzt die wie ein normales Panel,
|
||||
mit allen Mess-Typen die der Bridge handlet. Save/Cancel sind dort
|
||||
nicht standard; Fenster bleibt offen bis User es manuell schliesst.
|
||||
|
||||
Returns die Form-Instance."""
|
||||
|
||||
form = forms.Form()
|
||||
if title is None: title = mode.replace('_', ' ').title()
|
||||
form.Title = title
|
||||
try:
|
||||
form.ClientSize = drawing.Size(int(size[0]), int(size[1]))
|
||||
except Exception: pass
|
||||
form.Resizable = True
|
||||
form.Topmost = bool(topmost)
|
||||
|
||||
wv = forms.WebView()
|
||||
|
||||
if bridge is None:
|
||||
# Inline-Bridge fuer einfache Settings-Dialoge: SAVE/CANCEL, schliesse
|
||||
# bei beiden das Fenster.
|
||||
class _SatelliteBridge(BaseBridge):
|
||||
def __init__(self):
|
||||
BaseBridge.__init__(self, mode)
|
||||
def handle(self, data):
|
||||
t = data.get("type", "")
|
||||
p = data.get("payload") or {}
|
||||
if t == "READY":
|
||||
pass
|
||||
elif t == "SAVE":
|
||||
if on_save is not None:
|
||||
try: on_save(p)
|
||||
except Exception as ex:
|
||||
print("[{}] on_save: {}".format(_tag(mode), ex))
|
||||
try: form.Close()
|
||||
except Exception: pass
|
||||
elif t == "CANCEL":
|
||||
if on_cancel is not None:
|
||||
try: on_cancel()
|
||||
except Exception: pass
|
||||
try: form.Close()
|
||||
except Exception: pass
|
||||
bridge = _SatelliteBridge()
|
||||
bridge.set_webview(wv)
|
||||
|
||||
def on_title_(s, e):
|
||||
title_str = e.Title or ""
|
||||
if not title_str.startswith("RHINOMSG::"):
|
||||
return
|
||||
try:
|
||||
bridge.handle_raw(title_str[10:])
|
||||
except Exception as ex:
|
||||
print("[{}] Message-Fehler: {}".format(_tag(mode), ex))
|
||||
finally:
|
||||
try:
|
||||
wv.ExecuteScript("document.title='{}';".format(_tag(mode)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_loaded(s, e):
|
||||
try: wv.ExecuteScript("window.RHINO_MODE=true;")
|
||||
except Exception: pass
|
||||
try:
|
||||
wv.ExecuteScript(
|
||||
"var _ds=document.createElement('style');"
|
||||
"_ds.textContent='*{-webkit-user-select:none!important;user-select:none!important;}';"
|
||||
"document.head.appendChild(_ds);"
|
||||
"document.addEventListener('contextmenu',function(e){e.preventDefault();},true);"
|
||||
)
|
||||
except Exception: pass
|
||||
|
||||
wv.DocumentTitleChanged += on_title_
|
||||
wv.DocumentLoaded += on_loaded
|
||||
|
||||
form.Content = wv
|
||||
form.Show()
|
||||
# HTML nach Show() laden — sonst ist die WebView eventuell noch nicht
|
||||
# gerendert und die JS-Bridge initialisiert sich seltsam.
|
||||
try:
|
||||
load_inline(wv, mode, params=params)
|
||||
except Exception as ex:
|
||||
print("[{}] Inline-Fehler: {}".format(_tag(mode), ex))
|
||||
return form
|
||||
|
||||
|
||||
# --- Dynamic .NET Type ------------------------------------------------------
|
||||
|
||||
def create_dockable_type(guid_str, type_name, assembly_name):
|
||||
@@ -332,65 +577,277 @@ def _hex_rgb(h):
|
||||
|
||||
|
||||
_ICON_CACHE_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel/icons")
|
||||
# Versand-Icons im Projekt-Repo. PRIO 1: PNG (vorgerendert, weiss-auf-
|
||||
# transparent). PRIO 2: SVG (Mac-Rhino kann's via NSImage manchmal nicht
|
||||
# rendern, daher als Fallback ueber das Font-Glyph hinaus). Beide Ordner
|
||||
# werden gechecked.
|
||||
_PANEL_ICONS_PNG_DIR = os.path.join(
|
||||
os.path.dirname(_HERE), "icons_export", "panel_icons", "png")
|
||||
_PANEL_ICONS_SVG_DIR = os.path.join(
|
||||
os.path.dirname(_HERE), "icons_export", "panel_icons", "svg")
|
||||
|
||||
|
||||
def make_panel_icon(letter, bg_hex):
|
||||
"""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.
|
||||
"""
|
||||
def _try_load_png_white(png_path, size):
|
||||
"""PNG-Datei direkt als Bitmap laden + auf size skalieren. Geht auf
|
||||
allen Rhino-Versionen zuverlaessig (PNG-Support ist universal)."""
|
||||
try:
|
||||
size = 32 # 32x32 fuer Retina (wird auf 16pt skaliert dargestellt)
|
||||
bmp_src = drawing.Bitmap(png_path)
|
||||
if bmp_src is None: return None
|
||||
target = drawing.Bitmap(size, size,
|
||||
drawing.PixelFormat.Format32bppRgba)
|
||||
g = drawing.Graphics(target)
|
||||
try:
|
||||
try: g.AntiAlias = True
|
||||
except Exception: pass
|
||||
g.DrawImage(bmp_src, 0, 0, size, size)
|
||||
finally:
|
||||
g.Dispose()
|
||||
return target
|
||||
except Exception as ex:
|
||||
print("[CORE] PNG-load failed:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def _try_load_svg_white(svg_path, size):
|
||||
"""Laedt eine SVG-Datei und rendert sie als 32x32-Bitmap mit weisser
|
||||
Fuell-Farbe. Strategie: SVG-Text einlesen, fill="white" in alle <path>-
|
||||
Elemente injizieren, in den Icon-Cache als temp .svg schreiben und via
|
||||
Eto.Drawing.Bitmap(path) laden — auf Mac geht das via NSImage das SVGs
|
||||
seit macOS 10.14 unterstuetzt. Liefert Bitmap oder None."""
|
||||
try:
|
||||
with open(svg_path, "rb") as f:
|
||||
txt = f.read().decode("utf-8")
|
||||
# Path-Elemente weiss faerben (Material-Symbols-SVGs haben default-
|
||||
# black fill). Naive String-Manipulation reicht — die SVGs sind
|
||||
# einfach gestrickt (genau ein <path>).
|
||||
if 'fill=' not in txt:
|
||||
txt = txt.replace("<path ", '<path fill="#ffffff" ')
|
||||
if not os.path.isdir(_ICON_CACHE_DIR):
|
||||
os.makedirs(_ICON_CACHE_DIR)
|
||||
safe = re.sub(r"[^A-Za-z0-9]", "_",
|
||||
os.path.splitext(os.path.basename(svg_path))[0])
|
||||
tmp_path = os.path.join(_ICON_CACHE_DIR, "_svg_" + safe + ".svg")
|
||||
with open(tmp_path, "wb") as f:
|
||||
f.write(txt.encode("utf-8"))
|
||||
# Eto.Drawing.Bitmap aus File-Pfad: nutzt auf Mac NSImage (kann SVG).
|
||||
try:
|
||||
bmp_src = drawing.Bitmap(tmp_path)
|
||||
except Exception:
|
||||
bmp_src = None
|
||||
if bmp_src is None: return None
|
||||
# Auf size x size skalieren — die meisten Material-Symbols haben
|
||||
# einen 24-Einheiten-Viewbox, wir wollen 32px Output.
|
||||
target = drawing.Bitmap(size, size,
|
||||
drawing.PixelFormat.Format32bppRgba)
|
||||
g = drawing.Graphics(target)
|
||||
try:
|
||||
try: g.AntiAlias = True
|
||||
except Exception: pass
|
||||
# Transparenter Hintergrund — der Caller composited spaeter
|
||||
# ueber den farbigen Panel-Hintergrund.
|
||||
g.DrawImage(bmp_src, 0, 0, size, size)
|
||||
finally:
|
||||
g.Dispose()
|
||||
return target
|
||||
except Exception as ex:
|
||||
print("[CORE] SVG-load failed:", ex)
|
||||
return None
|
||||
|
||||
|
||||
# Material Symbols Outlined Codepoints fuer die Panel-Icons.
|
||||
# Quelle: https://fonts.google.com/icons (Codepoint-Tab pro Icon)
|
||||
# Wenn die Font "Material Symbols Outlined" installiert ist (Mac:
|
||||
# ~/Library/Fonts/MaterialSymbolsOutlined-Regular.ttf), werden diese
|
||||
# Glyphen gerendert. Sonst Fallback auf den ersten Buchstaben.
|
||||
_MATERIAL_CODEPOINTS = {
|
||||
"foundation": 0xf200,
|
||||
"view_in_ar": 0xe9fe,
|
||||
"palette": 0xe40a,
|
||||
"settings": 0xe8b8,
|
||||
"straighten": 0xe41c,
|
||||
"crop": 0xe3be,
|
||||
"view_quilt": 0xe8f9,
|
||||
"tune": 0xe429,
|
||||
"filter_alt": 0xef4f,
|
||||
"build": 0xe869,
|
||||
"construction": 0xea3c,
|
||||
"aspect_ratio": 0xe85b,
|
||||
"rule": 0xf1c2,
|
||||
"layers": 0xe53b,
|
||||
"menu": 0xe5d2,
|
||||
"design_services": 0xf10a,
|
||||
"square_foot": 0xea49,
|
||||
"dashboard": 0xe871,
|
||||
"category": 0xe574,
|
||||
}
|
||||
|
||||
_MATERIAL_FONT_NAMES = (
|
||||
"Material Symbols Outlined",
|
||||
"Material Symbols Rounded",
|
||||
"Material Icons", # alter Web-Font
|
||||
)
|
||||
|
||||
|
||||
def _try_material_font():
|
||||
"""Probiert die Material-Schrift-Namen durch und liefert den ersten der
|
||||
sich als FontFamily laden laesst — None wenn keiner installiert."""
|
||||
for fam in _MATERIAL_FONT_NAMES:
|
||||
try:
|
||||
ff = drawing.FontFamily(fam)
|
||||
if ff is not None: return fam
|
||||
except Exception: continue
|
||||
return None
|
||||
|
||||
|
||||
def _draw_glyph(g, size, font, glyph, fg):
|
||||
"""Zeichnet Text mittig auf eine Graphics-Surface."""
|
||||
try:
|
||||
ts = g.MeasureString(font, glyph)
|
||||
tx = (size - ts.Width) / 2
|
||||
ty = (size - ts.Height) / 2
|
||||
except Exception:
|
||||
tx, ty = size * 0.18, size * 0.12
|
||||
g.DrawText(font, fg, float(tx), float(ty), glyph)
|
||||
|
||||
|
||||
def make_panel_icon(name_or_letter, bg_hex):
|
||||
"""Erzeugt ein 32x32 Panel-Icon. `name_or_letter` kann ein Material-
|
||||
Icon-Name (z.B. 'foundation', 'palette') ODER ein einzelner Buchstabe
|
||||
sein. Bei Material-Namen wird die Material-Schrift verwendet; Fallback
|
||||
auf den ersten Buchstaben wenn die Schrift nicht installiert ist."""
|
||||
try:
|
||||
size = 32
|
||||
bmp = drawing.Bitmap(size, size, drawing.PixelFormat.Format32bppRgba)
|
||||
g = drawing.Graphics(bmp)
|
||||
used_material = False
|
||||
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)
|
||||
|
||||
# 0) Versand-Icons aus dem Repo bevorzugen. Zuerst PNG (geht
|
||||
# auf allen Rhino-Versionen sicher), sonst SVG-Fallback (NSImage
|
||||
# auf Mac, klappt nur manchmal).
|
||||
used_svg = False
|
||||
icon_bmp = None
|
||||
chosen_path = ""
|
||||
try:
|
||||
font = drawing.Font(drawing.FontFamilies.Sans, 18, drawing.FontStyle.Bold)
|
||||
except Exception:
|
||||
font = drawing.Font("Helvetica", 18, drawing.FontStyle.Bold)
|
||||
png_path = os.path.join(_PANEL_ICONS_PNG_DIR,
|
||||
name_or_letter + ".png")
|
||||
if os.path.isfile(png_path):
|
||||
icon_bmp = _try_load_png_white(png_path, size - 8)
|
||||
if icon_bmp is not None: chosen_path = png_path
|
||||
else: print("[CORE] PNG loaded but Bitmap is None:",
|
||||
png_path)
|
||||
# PNG-not-found ist normal: Fallback auf SVG dann Material-Font.
|
||||
# Nur loggen wenn final ALLES failt (s.u.).
|
||||
if icon_bmp is None:
|
||||
svg_path = os.path.join(_PANEL_ICONS_SVG_DIR,
|
||||
name_or_letter + ".svg")
|
||||
if os.path.isfile(svg_path):
|
||||
icon_bmp = _try_load_svg_white(svg_path, size - 8)
|
||||
if icon_bmp is not None: chosen_path = svg_path
|
||||
if icon_bmp is not None:
|
||||
pad = 4
|
||||
try:
|
||||
text_size = g.MeasureString(font, letter)
|
||||
tx = (size - text_size.Width) / 2
|
||||
ty = (size - text_size.Height) / 2
|
||||
g.DrawImage(icon_bmp, pad, pad,
|
||||
size - 2*pad, size - 2*pad)
|
||||
used_svg = True
|
||||
used_material = True # → kein Letter-Fallback
|
||||
print("[CORE] Icon path: {} ← {}".format(
|
||||
name_or_letter, chosen_path))
|
||||
except Exception as ex:
|
||||
print("[CORE] Icon composite error:", ex)
|
||||
except Exception as ex:
|
||||
print("[CORE] Icon path check error:", ex)
|
||||
|
||||
# 1) Material-Icon-Font (wenn keine SVG present)
|
||||
mat_cp = _MATERIAL_CODEPOINTS.get(name_or_letter)
|
||||
if not used_svg and mat_cp is not None:
|
||||
font_family_name = _try_material_font()
|
||||
if font_family_name:
|
||||
try:
|
||||
ff = drawing.FontFamily(font_family_name)
|
||||
# FontStyle.None: in Python3 ist None ein Keyword, deshalb
|
||||
# via System.Enum.ToObject explizit konstruieren — Python.NET 3
|
||||
# konvertiert int → Enum nicht mehr implizit.
|
||||
import System
|
||||
fs = System.Enum.ToObject(drawing.FontStyle, 0)
|
||||
font = drawing.Font(ff, 20, fs)
|
||||
glyph = chr(mat_cp)
|
||||
_draw_glyph(g, size, font, glyph,
|
||||
drawing.Colors.White)
|
||||
used_material = True
|
||||
except Exception as ex:
|
||||
print("[CORE] Material render error:", ex)
|
||||
used_material = False
|
||||
|
||||
# 2) Fallback: Buchstabe (erstes Zeichen bzw. eingegebener Buchstabe)
|
||||
if not used_material:
|
||||
letter = (name_or_letter[:1].upper()
|
||||
if name_or_letter else "?")
|
||||
try:
|
||||
font = drawing.Font(drawing.FontFamilies.Sans, 18,
|
||||
drawing.FontStyle.Bold)
|
||||
except Exception:
|
||||
tx, ty = size * 0.18, size * 0.12
|
||||
g.DrawText(font, drawing.Colors.White, float(tx), float(ty), letter)
|
||||
font = drawing.Font("Helvetica", 18,
|
||||
drawing.FontStyle.Bold)
|
||||
_draw_glyph(g, size, font, letter, drawing.Colors.White)
|
||||
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("#")))
|
||||
if used_svg: tag = "svg"
|
||||
elif used_material: tag = "mat"
|
||||
else: tag = "ltr"
|
||||
safe = re.sub(r"[^A-Za-z0-9]", "_", name_or_letter)
|
||||
path = os.path.join(_ICON_CACHE_DIR, "icon_{}_{}_{}.png".format(
|
||||
tag, safe, bg_hex.lstrip("#")))
|
||||
bmp.Save(path, drawing.ImageFormat.Png)
|
||||
except Exception as ex:
|
||||
print("[panel_base] Icon-Save:", ex)
|
||||
print("[CORE] Icon save error:", ex)
|
||||
path = None
|
||||
# 1. Versuch: Icon aus Datei-Pfad
|
||||
# WICHTIG: Mac Rhinos RegisterPanel meldet "expected Icon, got Icon"
|
||||
# wenn wir Eto.Drawing.Icon uebergeben — die API erwartet
|
||||
# System.Drawing.Icon. Daher zuerst System.Drawing probieren,
|
||||
# dann Eto als Fallback.
|
||||
if path and os.path.isfile(path):
|
||||
try:
|
||||
return drawing.Icon(path)
|
||||
import System.Drawing as _sd
|
||||
ic = _sd.Icon(path)
|
||||
print("[CORE] Icon created via System.Drawing.Icon(path) [{}]".format(tag))
|
||||
return ic
|
||||
except Exception as ex:
|
||||
print("[panel_base] Icon(path) fehlgeschlagen:", ex)
|
||||
# 2. Versuch: Icon(scale, bitmap)
|
||||
print("[CORE] System.Drawing.Icon(path) failed:", ex)
|
||||
# System.Drawing.Bitmap als Fallback (manche RegisterPanel-Overloads akzeptieren Bitmap)
|
||||
try:
|
||||
return drawing.Icon(1.0, bmp)
|
||||
except Exception: pass
|
||||
# 3. Versuch: Icon(bitmap)
|
||||
import System.Drawing as _sd
|
||||
bmp_sd = _sd.Bitmap(path)
|
||||
print("[CORE] Icon created via System.Drawing.Bitmap(path) [{}]".format(tag))
|
||||
return bmp_sd
|
||||
except Exception as ex:
|
||||
print("[CORE] System.Drawing.Bitmap(path) failed:", ex)
|
||||
# Eto.Drawing.Icon als letzter Versuch — falls Rhino-Version anders ist
|
||||
try:
|
||||
return drawing.Icon(bmp)
|
||||
ic = drawing.Icon(path)
|
||||
print("[CORE] Icon erzeugt via Eto.Drawing.Icon(path) [{}]".format(tag))
|
||||
return ic
|
||||
except Exception as ex:
|
||||
print("[CORE] Eto.Drawing.Icon(path) failed:", ex)
|
||||
# Bitmap-Fallback (in-memory) — wenn alles vorherige fehlschlaegt
|
||||
try:
|
||||
ic = drawing.Icon(1.0, bmp)
|
||||
print("[CORE] Icon erzeugt via Eto.Drawing.Icon(scale, bmp) [{}]".format(tag))
|
||||
return ic
|
||||
except Exception: pass
|
||||
# 4. Fallback: einfach das Bitmap zurueck (Rhino akzeptiert ggf. das auch)
|
||||
print("[CORE] Icon Fallback: Eto.Bitmap zurueck ({})".format(tag))
|
||||
return bmp
|
||||
except Exception as ex:
|
||||
print("[panel_base] Icon-Erstellung fehlgeschlagen:", ex)
|
||||
print("[CORE] Icon-Erstellung failed:", ex)
|
||||
return None
|
||||
|
||||
|
||||
@@ -404,7 +861,7 @@ def find_plugin():
|
||||
if p is not None:
|
||||
return p
|
||||
except Exception as ex:
|
||||
print("[panel_base] Plugin-Suche:", ex)
|
||||
print("[CORE] Plugin-Suche:", ex)
|
||||
return None
|
||||
|
||||
|
||||
@@ -417,13 +874,15 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
|
||||
Bei docked Panels wirkt's als Hint, bei float Panels als
|
||||
tatsaechliche Startgroesse.
|
||||
"""
|
||||
t_outer = time.time()
|
||||
sticky_reg = "panel_registered_" + mode
|
||||
sticky_guid = "panel_guid_" + mode
|
||||
|
||||
if not sc.sticky.get(sticky_reg):
|
||||
t_reg = time.time()
|
||||
plugin = find_plugin()
|
||||
if plugin is None:
|
||||
print("[{}] Plugin nicht gefunden".format(mode.upper()))
|
||||
print("[{}] Plugin not found".format(_tag(mode)))
|
||||
return
|
||||
try:
|
||||
type_name = "DynPanel_" + mode
|
||||
@@ -437,7 +896,7 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
|
||||
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))
|
||||
print("[{}] MinimumSize konnte nicht set werden: {}".format(_tag(mode), ex))
|
||||
# Auf einigen Eto-Versionen gibt es zusaetzlich Size/ClientSize
|
||||
for attr in ("Size", "ClientSize"):
|
||||
try:
|
||||
@@ -452,16 +911,18 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
|
||||
)
|
||||
icon = None
|
||||
if icon_spec:
|
||||
t_icon = time.time()
|
||||
try:
|
||||
icon = make_panel_icon(icon_spec[0], icon_spec[1])
|
||||
except Exception as ex:
|
||||
print("[{}] Icon-Erstellung uebersprungen: {}".format(mode.upper(), ex))
|
||||
print("[{}] Icon-Erstellung uebersprungen: {}".format(_tag(mode), ex))
|
||||
icon = None
|
||||
_t_mark("icon", mode, t_icon)
|
||||
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).
|
||||
# Mac not available 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:
|
||||
@@ -470,30 +931,34 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
|
||||
registered = True
|
||||
registered_with_icon = with_icon
|
||||
if with_icon:
|
||||
print("[{}] Panel mit Icon registriert ({})".format(
|
||||
mode.upper(), type(arg).__name__))
|
||||
print("[{}] Panel registered with icon ({})".format(
|
||||
_tag(mode), type(arg).__name__))
|
||||
break
|
||||
except Exception as ex:
|
||||
if with_icon:
|
||||
print("[{}] RegisterPanel mit Icon fehlgeschlagen: {}".format(
|
||||
mode.upper(), ex))
|
||||
print("[{}] RegisterPanel mit Icon failed: {}".format(
|
||||
_tag(mode), ex))
|
||||
else:
|
||||
print("[{}] RegisterPanel fehlgeschlagen: {}".format(
|
||||
mode.upper(), ex))
|
||||
print("[{}] RegisterPanel failed: {}".format(
|
||||
_tag(mode), ex))
|
||||
if registered and not registered_with_icon and icon is not None:
|
||||
print("[{}] Panel ohne Icon registriert (Fallback)".format(mode.upper()))
|
||||
print("[{}] Panel ohne Icon registriert (Fallback)".format(_tag(mode)))
|
||||
if not registered:
|
||||
return
|
||||
sc.sticky[sticky_reg] = True
|
||||
sc.sticky[sticky_guid] = System.Guid(guid_str)
|
||||
print("[{}] Panel registriert".format(mode.upper()))
|
||||
print("[{}] Panel registered".format(_tag(mode)))
|
||||
except Exception as ex:
|
||||
print("[{}] Registrierung fehlgeschlagen: {}".format(mode.upper(), ex))
|
||||
print("[{}] Registrierung failed: {}".format(_tag(mode), ex))
|
||||
return
|
||||
_t_mark("register", mode, t_reg)
|
||||
|
||||
try:
|
||||
t_open = time.time()
|
||||
guid = sc.sticky.get(sticky_guid, System.Guid(guid_str))
|
||||
RhinoUI.Panels.OpenPanel(guid)
|
||||
print("[{}] Panel geoeffnet".format(mode.upper()))
|
||||
_t_mark("OpenPanel", mode, t_open)
|
||||
print("[{}] Panel opened".format(_tag(mode)))
|
||||
except Exception as ex:
|
||||
print("[{}] OpenPanel fehlgeschlagen: {}".format(mode.upper(), ex))
|
||||
print("[{}] OpenPanel failed: {}".format(_tag(mode), ex))
|
||||
_t_mark("register_and_open", mode, t_outer)
|
||||
|
||||
@@ -1,798 +0,0 @@
|
||||
# ! 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"))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user