From f1860ae85d1197a62bdf21763f8f52b0948817fb Mon Sep 17 00:00:00 2001 From: karim Date: Tue, 26 May 2026 23:17:08 +0200 Subject: [PATCH] =?UTF-8?q?Raumstempel-Stile=20(Presets)=20=E2=80=94=20spe?= =?UTF-8?q?ichern=20+=20anwenden=20per=20Doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Damit User wiederkehrende Stempel-Configs (Wettbewerb / Bauantrag / Mobiliar etc.) nicht jedes Mal neu klicken muss. Backend (elemente.py): - Storage in doc.Strings dossier_raum_stempel_stile als JSON-Array - load_raum_stempel_stile / save_raum_stempel_stile Helpers - Bridge-Handler: - SAVE_RAUM_STIL: upsert by id (neu wenn leer) - DELETE_RAUM_STIL: remove by id - APPLY_RAUM_STIL: schreibt Stil-Felder auf Raum-IDs + Regen - _RAUM_STIL_FIELDS umfasst font/bold/italic/txtH/txtModus/align/ rundung/fuellung/showSia/layout (alles was die Optik bestimmt) - raumStempelStile im STATE-Emit zum Frontend Frontend: - saveRaumStil/deleteRaumStil/applyRaumStil in rhinoBridge.js - RaumProperties: neue "Stil"-Sektion oben mit Dropdown * gespeicherte Stile + "+ Aktuelle Settings als Stil speichern" + "🗑 Aktiven Stil loeschen" * Klick auf Stil → applyRaumStil mit aktueller Raum-ID - Beide PropertiesView-Aufrufe (inline + satellite) bekommen die Liste --- rhino/elemente.py | 148 ++++++++++++++++++++++++++++++++++ src/ElementeApp.jsx | 61 +++++++++++++- src/ElementePropertiesApp.jsx | 1 + src/lib/rhinoBridge.js | 8 ++ 4 files changed, 215 insertions(+), 3 deletions(-) diff --git a/rhino/elemente.py b/rhino/elemente.py index c57cb7d..ed9fc39 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -330,6 +330,64 @@ _RAUM_FIELD_IDS = ("nummer", "name", "funktion", "area", "sia") # Default-Layout (entspricht dem alten Verhalten) _RAUM_LAYOUT_DEFAULT = [["nummer", "name"], ["funktion"], ["area"]] +# Per-Document Storage-Key fuer Raumstempel-Stile (Presets). +_KEY_RAUM_STILE = "dossier_raum_stempel_stile" + +# Felder die ein Stil persistiert (= alles was die Optik bestimmt). Nicht +# enthalten: Name/Nummer/Funktion (inhaltlich pro Raum), area/umfang (aus +# Geometrie berechnet), Geschoss (Raum-Position). +_RAUM_STIL_FIELDS = ( + "font", "bold", "italic", + "txtH", "txtModus", + "align", "rundung", + "fuellung", + "showSia", + "layout", +) + + +def load_raum_stempel_stile(doc): + """Liest alle Raumstempel-Stile aus doc.Strings als Liste of dicts. + Schema pro Stil: {id, name, <_RAUM_STIL_FIELDS>}. Leer wenn keine. + """ + if doc is None: return [] + try: + raw = doc.Strings.GetValue(_KEY_RAUM_STILE) or "" + except Exception: raw = "" + if not raw: return [] + try: + import json as _json + data = _json.loads(raw) + if not isinstance(data, list): return [] + out = [] + for s in data: + if not isinstance(s, dict): continue + if not s.get("id") or not s.get("name"): continue + out.append(s) + return out + except Exception as ex: + print("[ELEMENTE] load_raum_stempel_stile:", ex) + return [] + + +def save_raum_stempel_stile(doc, stile): + """Persistiert die Stil-Liste in doc.Strings.""" + if doc is None or not isinstance(stile, list): return False + try: + import json as _json + clean = [] + for s in stile: + if not isinstance(s, dict): continue + if not s.get("id") or not s.get("name"): continue + clean.append(s) + doc.Strings.SetString(_KEY_RAUM_STILE, + _json.dumps(clean, ensure_ascii=False)) + return True + except Exception as ex: + print("[ELEMENTE] save_raum_stempel_stile:", ex) + return False + + _RAUM_RUNDUNGEN = ("exakt", "0.01", "0.1", "0.5", "1") @@ -6382,6 +6440,17 @@ class ElementeBridge(panel_base.BaseBridge): delete_oeff_style(doc, p.get("id")) except Exception as ex: print("[ELEMENTE] del oeff style:", ex) self._send_state() + elif t == "SAVE_RAUM_STIL": + # Speichert aktuelle Raum-Stil-Settings (vom Frontend gepacked) + # unter id (neu wenn leer) + name. Liste persistiert in + # doc.Strings, state-emit pushed neue Liste zum UI. + self._cmd_save_raum_stil(p) + elif t == "DELETE_RAUM_STIL": + self._cmd_delete_raum_stil(p) + elif t == "APPLY_RAUM_STIL": + # Wendet einen gespeicherten Stil auf die im Patch genannten + # raum_outline element_ids an. p = {stilId, ids=[…]}. + self._cmd_apply_raum_stil(p) elif t == "OPEN_ELEMENTE_UEBERSICHT": try: import elemente_uebersicht @@ -6647,6 +6716,7 @@ class ElementeBridge(panel_base.BaseBridge): {"name": n, "color": m["color"]} for n, m in _get_all_materials(doc).items()], "oeffStyles": list_oeff_styles(doc), + "raumStempelStile": load_raum_stempel_stile(doc), } self.send("STATE", payload) # An Properties-Satellite-Window forwarden falls offen @@ -8464,6 +8534,84 @@ class ElementeBridge(panel_base.BaseBridge): size=(640, 560), bridge=b) + def _cmd_save_raum_stil(self, p): + """Speichert einen Raumstempel-Stil. Payload: + {id?: string, name: string, settings: {font, bold, italic, txtH, + txtModus, align, rundung, fuellung, showSia, layout}} + Wenn id leer → neuer Stil mit uuid. State-Emit anschliessend. + """ + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + name = (p.get("name") or "").strip() + if not name: + print("[ELEMENTE] SAVE_RAUM_STIL: name leer") + return + settings = p.get("settings") or {} + sid = (p.get("id") or "").strip() or ("stil_" + uuid.uuid4().hex[:8]) + stile = load_raum_stempel_stile(doc) + # Upsert by id + new_stil = {"id": sid, "name": name} + for f in _RAUM_STIL_FIELDS: + if f in settings: + new_stil[f] = settings[f] + found = False + for i, s in enumerate(stile): + if s.get("id") == sid: + stile[i] = new_stil; found = True; break + if not found: + stile.append(new_stil) + if save_raum_stempel_stile(doc, stile): + print("[ELEMENTE] Stempel-Stil '{}' ({}) gespeichert".format( + name, sid)) + self._send_state() + + def _cmd_delete_raum_stil(self, p): + """Loescht einen Stil per id. Payload: {id}.""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + sid = (p.get("id") or "").strip() + if not sid: return + stile = load_raum_stempel_stile(doc) + stile = [s for s in stile if s.get("id") != sid] + if save_raum_stempel_stile(doc, stile): + print("[ELEMENTE] Stempel-Stil {} geloescht".format(sid)) + self._send_state() + + def _cmd_apply_raum_stil(self, p): + """Wendet einen Stil auf eine Liste von raum_outline-Element-IDs + an. Payload: {stilId, ids: [element_id, ...]}. Schreibt die Stil- + Felder auf die Source-Outlines + triggert Regen pro Raum. + """ + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + sid = (p.get("stilId") or "").strip() + ids = p.get("ids") or [] + if not sid or not ids: return + stile = load_raum_stempel_stile(doc) + stil = next((s for s in stile if s.get("id") == sid), None) + if stil is None: + print("[ELEMENTE] APPLY_RAUM_STIL: Stil {} nicht gefunden".format(sid)) + return + # Stil-Felder → Patch fuer _update_wall-Pfad + patch_base = {} + for f in _RAUM_STIL_FIELDS: + if f in stil: + patch_base[f] = stil[f] + n_applied = 0 + for eid in ids: + try: + # _update_wall braucht id + alle Patch-Felder + patch = dict(patch_base) + patch["id"] = eid + self._update_wall(patch) + n_applied += 1 + except Exception as ex: + print("[ELEMENTE] apply stil {} -> {}: {}".format(sid, eid, ex)) + if n_applied > 0: + print("[ELEMENTE] Stempel-Stil '{}' auf {} Raum/Raeume angewendet".format( + stil.get("name"), n_applied)) + self._send_state() + def _cmd_create_symbol(self, p): """Platziert ein Library-Item (symbol/object) im Doc. Interactive GetPoint im aktiven Viewport — User klickt Position. diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx index 446e137..147a8b9 100644 --- a/src/ElementeApp.jsx +++ b/src/ElementeApp.jsx @@ -11,6 +11,7 @@ import { openSwisstopo, openSwisstopoDialog, openOsmDialog, updateElement, deleteElement, openElementeUebersicht, openElementeProperties, saveOeffStyle, deleteOeffStyle, + saveRaumStil, deleteRaumStil, applyRaumStil, listLibrary, } from './lib/rhinoBridge' @@ -506,7 +507,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) { // PropertiesView: gemeinsame Komponente, rendert die passende Property- // Form je nach Element-Typ. Wiederverwendbar in Inline + Satellite-Window. -export function PropertiesView({ selected, geschosse, materials, hatchPatterns, oeffStyles, fonts }) { +export function PropertiesView({ selected, geschosse, materials, hatchPatterns, oeffStyles, fonts, raumStempelStile }) { if (!selected) return null const upd = (p) => updateElement(selected.id, p) const del = (label) => () => { if (window.confirm(`${label} löschen?`)) deleteElement(selected.id) } @@ -529,6 +530,7 @@ export function PropertiesView({ selected, geschosse, materials, hatchPatterns, if (selected.kind === 'raum') return if (selected.kind === 'aussparung') @@ -583,7 +585,8 @@ export default function ElementeApp() { materials={state.materials || []} hatchPatterns={state.hatchPatterns} fonts={state.fonts || []} - oeffStyles={state.oeffStyles || []} /> + oeffStyles={state.oeffStyles || []} + raumStempelStile={state.raumStempelStile || []} /> )} { + if (val === '__save__') { + const n = (window.prompt('Name für neuen Stempel-Stil:', 'Stil') || '').trim() + if (!n) return + saveRaumStil('', n, { + font: raum.font || '', + bold: !!raum.bold, + italic: !!raum.italic, + txtH: raum.txtH, + txtModus: raum.txtModus || 'fix', + align: raum.align || 'mid', + rundung: raum.rundung || '', + fuellung: raum.fuellung || '', + showSia: !!raum.showSia, + layout: Array.isArray(raum.layout) ? raum.layout : [], + }) + return + } + if (val === '__delete__') { + if (activeStilId && + window.confirm(`Stil "${stilList.find(s => s.id === activeStilId)?.name}" löschen?`)) + deleteRaumStil(activeStilId) + return + } + if (val) applyRaumStil(val, [raum.id]) + } + const [name, setName] = useState(raum.name || 'Raum') const [nummer, setNummer] = useState(raum.nummer || '') const [funktion, setFunktion] = useState(raum.funktion || '') @@ -944,6 +980,25 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo + {/* Stempel-Stil Preset-Picker — apply einer gespeicherten Visual-Vorlage + auf den Raum. "+ Aktuell speichern" frischt die Liste mit den + jetzigen Settings als neuen Stil. Stile sind per-Doc. */} +
+ Stil + +
+
Geschoss