From 975071c9954b221be5116bde61abca48e8ca109d Mon Sep 17 00:00:00 2001 From: karim Date: Wed, 27 May 2026 00:36:31 +0200 Subject: [PATCH] Stempel: Stile + Layout-Customisation (Header, Show-Toggles) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bilanz-Stempel hat jetzt: - Custom Header (Default "Nutzflächen", frei editierbar) - Per-Stempel Show-Toggles: scope, hnf, nnf, nf, vf, ff, ngf, gf, agf, count (Anzahl Raeume), separators (Trennlinien) - Stempel-Stile (Presets) per Doc — separate Storage von Raumstempel- Stilen (dossier_stempel_stile) Backend (elemente.py): - UserStrings: dossier_stempel_header + dossier_stempel_show_* (11 Flags) + dossier_stempel_stil_id - _make_stempel_text erweitert: header_text, show_scope, show_separators, show_count, visibility-dict - _format_bilanz_lines: visibility + show_separators + show_count Args - load_stempel_stile / save_stempel_stile (eigener doc.Strings-Key) - Bridge-Handler SAVE/DELETE/APPLY_STEMPEL_STIL (analog Raum-Stil-Pattern, inkl. Bulk-Apply via Selection + applyToIds-Mechanism wie SaveRaum-Stil) - _sync_stempel_to_source spiegelt Font/Bold/Italic/TextHeight vom Live- TextEntity zur UserString-Storage (Oberleiste-Edits ueberleben Regen) - _update_wall stempel-Branch um header + show-Flags erweitert Frontend (StempelProperties): - Stil-Picker-Dropdown analog Raum (Stil wählen / + speichern / 🗑 löschen) - Header-Input (Inline-Text) - 11 Show-Toggles als kompakte Grid (Check-Box-Stil) - Live-Vorschau respektiert Visibility-Flags + bilanz.count --- rhino/elemente.py | 424 +++++++++++++++++++++++++++++++--- src/ElementeApp.jsx | 140 +++++++++-- src/ElementePropertiesApp.jsx | 1 + src/lib/rhinoBridge.js | 9 + 4 files changed, 528 insertions(+), 46 deletions(-) diff --git a/rhino/elemente.py b/rhino/elemente.py index 4be53cd..54cdad6 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -2715,6 +2715,13 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, raum_layout=None, raum_txt_modus=None, stempel_scope=None, stempel_txt_h=None, stempel_font=None, stempel_bold=None, stempel_italic=None, + stempel_header=None, stempel_show_scope=None, + stempel_show_hnf=None, stempel_show_nnf=None, + stempel_show_nf=None, stempel_show_vf=None, + stempel_show_ff=None, stempel_show_ngf=None, + stempel_show_gf=None, stempel_show_agf=None, + stempel_show_count=None, stempel_show_seps=None, + stempel_stil_id=None, wand_layered=None, wand_layers=None, wand_layer_idx=None, wand_chain_members=None, aussp_parent=None): @@ -2934,6 +2941,25 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, if stempel_italic is not None: obj_attrs.SetUserString(_KEY_STEMPEL_ITAL, "1" if bool(stempel_italic) else "0") + if stempel_header is not None: + obj_attrs.SetUserString(_KEY_STEMPEL_HEADER, str(stempel_header)) + for _k, _v in ( + (_KEY_STEMPEL_SHOW_SCOPE, stempel_show_scope), + (_KEY_STEMPEL_SHOW_HNF, stempel_show_hnf), + (_KEY_STEMPEL_SHOW_NNF, stempel_show_nnf), + (_KEY_STEMPEL_SHOW_NF, stempel_show_nf), + (_KEY_STEMPEL_SHOW_VF, stempel_show_vf), + (_KEY_STEMPEL_SHOW_FF, stempel_show_ff), + (_KEY_STEMPEL_SHOW_NGF, stempel_show_ngf), + (_KEY_STEMPEL_SHOW_GF, stempel_show_gf), + (_KEY_STEMPEL_SHOW_AGF, stempel_show_agf), + (_KEY_STEMPEL_SHOW_COUNT, stempel_show_count), + (_KEY_STEMPEL_SHOW_SEPS, stempel_show_seps), + ): + if _v is not None: + obj_attrs.SetUserString(_k, "1" if bool(_v) else "0") + if stempel_stil_id is not None: + obj_attrs.SetUserString(_KEY_STEMPEL_STIL_ID, str(stempel_stil_id)) # Wand-Schichten if wand_layered is not None: obj_attrs.SetUserString(_KEY_WAND_LAYERED, @@ -3141,6 +3167,29 @@ def _read_meta(obj): st_font = a.GetUserString(_KEY_STEMPEL_FONT) or "" st_bold = (a.GetUserString(_KEY_STEMPEL_BOLD) == "1") st_ital = (a.GetUserString(_KEY_STEMPEL_ITAL) == "1") + # Stempel-Layout-Custom (Defaults: alle "show" on ausser show_count; + # leer/None → True ausser show_count → False) + st_header = a.GetUserString(_KEY_STEMPEL_HEADER) + if st_header is None or st_header == "": + st_header = "Nutzflächen" + def _sh_on(key, default=True): + v = a.GetUserString(key) + if default: + return v != "0" # None/""/"1" → True + else: + return v == "1" # nur explizites "1" → True + st_show_scope = _sh_on(_KEY_STEMPEL_SHOW_SCOPE, True) + st_show_hnf = _sh_on(_KEY_STEMPEL_SHOW_HNF, True) + st_show_nnf = _sh_on(_KEY_STEMPEL_SHOW_NNF, True) + st_show_nf = _sh_on(_KEY_STEMPEL_SHOW_NF, True) + st_show_vf = _sh_on(_KEY_STEMPEL_SHOW_VF, True) + st_show_ff = _sh_on(_KEY_STEMPEL_SHOW_FF, True) + st_show_ngf = _sh_on(_KEY_STEMPEL_SHOW_NGF, True) + st_show_gf = _sh_on(_KEY_STEMPEL_SHOW_GF, True) + st_show_agf = _sh_on(_KEY_STEMPEL_SHOW_AGF, True) + st_show_count = _sh_on(_KEY_STEMPEL_SHOW_COUNT, False) + st_show_seps = _sh_on(_KEY_STEMPEL_SHOW_SEPS, True) + st_stil_id = a.GetUserString(_KEY_STEMPEL_STIL_ID) or "" # Field-Layout — parsed JSON list of rows r_layout_raw = a.GetUserString(_KEY_RAUM_LAYOUT) or "" r_layout = [] @@ -3268,6 +3317,19 @@ def _read_meta(obj): "stempel_font": st_font, "stempel_bold": st_bold, "stempel_italic": st_ital, + "stempel_header": st_header, + "stempel_show_scope": st_show_scope, + "stempel_show_hnf": st_show_hnf, + "stempel_show_nnf": st_show_nnf, + "stempel_show_nf": st_show_nf, + "stempel_show_vf": st_show_vf, + "stempel_show_ff": st_show_ff, + "stempel_show_ngf": st_show_ngf, + "stempel_show_gf": st_show_gf, + "stempel_show_agf": st_show_agf, + "stempel_show_count": st_show_count, + "stempel_show_seps": st_show_seps, + "stempel_stil_id": st_stil_id, "wand_layered": w_layered, "wand_layers": w_layers, "wand_layer_idx": w_layer_idx, @@ -3892,6 +3954,58 @@ _KEY_STEMPEL_TXT_H = "dossier_stempel_txt_h" # Texthoehe in m _KEY_STEMPEL_FONT = "dossier_stempel_font" _KEY_STEMPEL_BOLD = "dossier_stempel_bold" _KEY_STEMPEL_ITAL = "dossier_stempel_italic" +# Layout-Customisation (per Stempel) +_KEY_STEMPEL_HEADER = "dossier_stempel_header" # default "Nutzflächen" +_KEY_STEMPEL_SHOW_SCOPE = "dossier_stempel_show_scope" # default "1" +_KEY_STEMPEL_SHOW_HNF = "dossier_stempel_show_hnf" # default "1" +_KEY_STEMPEL_SHOW_NNF = "dossier_stempel_show_nnf" # default "1" +_KEY_STEMPEL_SHOW_NF = "dossier_stempel_show_nf" # default "1" +_KEY_STEMPEL_SHOW_VF = "dossier_stempel_show_vf" # default "1" +_KEY_STEMPEL_SHOW_FF = "dossier_stempel_show_ff" # default "1" +_KEY_STEMPEL_SHOW_NGF = "dossier_stempel_show_ngf" # default "1" +_KEY_STEMPEL_SHOW_GF = "dossier_stempel_show_gf" # default "1" +_KEY_STEMPEL_SHOW_AGF = "dossier_stempel_show_agf" # default "1" +_KEY_STEMPEL_SHOW_COUNT = "dossier_stempel_show_count" # default "0" +_KEY_STEMPEL_SHOW_SEPS = "dossier_stempel_show_seps" # default "1" +_KEY_STEMPEL_STIL_ID = "dossier_stempel_stil_id" # aktiver Stil +# Storage-Key fuer Stempel-Stile (Presets, per Doc) +_KEY_STEMPEL_STILE = "dossier_stempel_stile" +_STEMPEL_STIL_FIELDS = ( + "header", "showScope", + "showHnf", "showNnf", "showNf", "showVf", "showFf", + "showNgf", "showGf", "showAgf", "showCount", "showSeparators", + "font", "bold", "italic", "txtH", +) + + +def load_stempel_stile(doc): + if doc is None: return [] + try: raw = doc.Strings.GetValue(_KEY_STEMPEL_STILE) or "" + except Exception: raw = "" + if not raw: return [] + try: + import json as _json + data = _json.loads(raw) + if not isinstance(data, list): return [] + return [s for s in data if isinstance(s, dict) + and s.get("id") and s.get("name")] + except Exception as ex: + print("[ELEMENTE] load_stempel_stile:", ex) + return [] + + +def save_stempel_stile(doc, stile): + if doc is None or not isinstance(stile, list): return False + try: + import json as _json + clean = [s for s in stile if isinstance(s, dict) + and s.get("id") and s.get("name")] + doc.Strings.SetString(_KEY_STEMPEL_STILE, + _json.dumps(clean, ensure_ascii=False)) + return True + except Exception as ex: + print("[ELEMENTE] save_stempel_stile:", ex) + return False # Oeffnungs-Cutout: Boolean-Difference aus Wand. Zusaetzlich kriegt die # Oeffnung ihr eigenes Volumen (Rahmen + Sims + Glas) als Sub-Element. @@ -5061,51 +5175,89 @@ def compute_sia_bilanz(doc, scope="total"): return out -def _format_bilanz_lines(bilanz, rundung="0.1"): +def _format_bilanz_lines(bilanz, rundung="0.1", visibility=None, + show_separators=True, show_count=False): """Baut die Stempel-Textzeilen aus einer Bilanz. Zeigt nur Kategorien - mit Flaeche > 0. Format kompakt: 'HNF 120.5 m²'.""" + mit Flaeche > 0 UND deren visibility-Flag True ist. + + visibility = dict mit Keys hnf/nnf/nf/vf/ff/ngf/gf/agf → True/False. + None heisst: alle anzeigen (Default). + """ + if visibility is None: + visibility = {} + def _v(k): return visibility.get(k, True) + sep = "────────────" lines = [] def _row(label, val): return "{} {} m²".format(label.ljust(4), _format_area(val, rundung)) # Nutzflaechen-Block - has_nf = bilanz["hnf"] > 0 or bilanz["nnf"] > 0 - if bilanz["hnf"] > 0: lines.append(_row("HNF", bilanz["hnf"])) - if bilanz["nnf"] > 0: lines.append(_row("NNF", bilanz["nnf"])) - if has_nf: - lines.append("────────────") + show_hnf = _v("hnf") and bilanz["hnf"] > 0 + show_nnf = _v("nnf") and bilanz["nnf"] > 0 + has_nf_inputs = show_hnf or show_nnf + if show_hnf: lines.append(_row("HNF", bilanz["hnf"])) + if show_nnf: lines.append(_row("NNF", bilanz["nnf"])) + if _v("nf") and has_nf_inputs and bilanz["nf"] > 0: + if show_separators: lines.append(sep) lines.append(_row("NF", bilanz["nf"])) # VF/FF - if bilanz["vf"] > 0: lines.append(_row("VF", bilanz["vf"])) - if bilanz["ff"] > 0: lines.append(_row("FF", bilanz["ff"])) + show_vf = _v("vf") and bilanz["vf"] > 0 + show_ff = _v("ff") and bilanz["ff"] > 0 + if show_vf: lines.append(_row("VF", bilanz["vf"])) + if show_ff: lines.append(_row("FF", bilanz["ff"])) # NGF wenn was zusammenkommt - if bilanz["ngf"] > 0 and (bilanz["vf"] > 0 or bilanz["ff"] > 0): - lines.append("────────────") + if _v("ngf") and bilanz["ngf"] > 0 and (show_vf or show_ff or has_nf_inputs): + if show_separators: lines.append(sep) lines.append(_row("NGF", bilanz["ngf"])) # Gross-Flaechen separat - if bilanz["gf"] > 0 or bilanz["agf"] > 0: - lines.append("────────────") - if bilanz["gf"] > 0: lines.append(_row("GF", bilanz["gf"])) - if bilanz["agf"] > 0: lines.append(_row("AGF", bilanz["agf"])) + show_gf = _v("gf") and bilanz["gf"] > 0 + show_agf = _v("agf") and bilanz["agf"] > 0 + if show_gf or show_agf: + if show_separators and lines: lines.append(sep) + if show_gf: lines.append(_row("GF", bilanz["gf"])) + if show_agf: lines.append(_row("AGF", bilanz["agf"])) + # Count optional + if show_count and bilanz.get("count", 0) > 0: + if show_separators and lines: lines.append(sep) + lines.append("{} Räume".format(bilanz["count"])) return lines def _make_stempel_text(pos, scope, doc, text_height=0.20, z=0.0, - font=None, bold=False, italic=False): + font=None, bold=False, italic=False, + header_text="Nutzflächen", show_scope=True, + show_separators=True, show_count=False, + visibility=None): """Baut die Stempel-TextEntity fuer einen Scope ("total" oder - "geschoss:"). Header = "Nutzflächen · {Scope-Label}".""" + "geschoss:"). header_text + show_* steuern Layout-Customisation. + + visibility = dict mit hnf/nnf/nf/vf/ff/ngf/gf/agf → True/False. + Default: alle True (= aktuelles Verhalten, nur Kategorien > 0). + """ try: bilanz = compute_sia_bilanz(doc, scope) - if scope == "total": - scope_label = "Total" - elif bilanz["geschossName"]: - scope_label = bilanz["geschossName"] - else: - scope_label = "—" - header = "Nutzflächen · {}".format(scope_label) - body = _format_bilanz_lines(bilanz) + # Header zusammenbauen + header_parts = [] + if header_text and header_text.strip(): + header_parts.append(header_text.strip()) + if show_scope: + if scope == "total": + scope_label = "Total" + elif bilanz["geschossName"]: + scope_label = bilanz["geschossName"] + else: + scope_label = "—" + header_parts.append(scope_label) + header = " · ".join(header_parts) if header_parts else "" + body = _format_bilanz_lines(bilanz, visibility=visibility, + show_separators=show_separators, + show_count=show_count) if not body: body = ["(keine klassifizierten Räume)"] - text = "\n".join([header, "════════════"] + body) + lines = [] + if header: lines.append(header) + if show_separators and header: lines.append("════════════") + lines.extend(body) + text = "\n".join(lines) te = rg.TextEntity() te.Text = text plane = rg.Plane(rg.Point3d(pos.X, pos.Y, float(z)), @@ -6588,9 +6740,26 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name font = meta.get("stempel_font", "") or "" bold = bool(meta.get("stempel_bold", False)) italic = bool(meta.get("stempel_italic", False)) - new_te = _make_stempel_text(pos, scope, doc, text_height=txt_h, - z=pos.Z, font=font, bold=bold, - italic=italic) + # Layout-Customisation aus Meta + header_text = meta.get("stempel_header", "Nutzflächen") or "Nutzflächen" + visibility = { + "hnf": bool(meta.get("stempel_show_hnf", True)), + "nnf": bool(meta.get("stempel_show_nnf", True)), + "nf": bool(meta.get("stempel_show_nf", True)), + "vf": bool(meta.get("stempel_show_vf", True)), + "ff": bool(meta.get("stempel_show_ff", True)), + "ngf": bool(meta.get("stempel_show_ngf", True)), + "gf": bool(meta.get("stempel_show_gf", True)), + "agf": bool(meta.get("stempel_show_agf", True)), + } + new_te = _make_stempel_text( + pos, scope, doc, text_height=txt_h, + z=pos.Z, font=font, bold=bold, italic=italic, + header_text=header_text, + show_scope=bool(meta.get("stempel_show_scope", True)), + show_separators=bool(meta.get("stempel_show_seps", True)), + show_count=bool(meta.get("stempel_show_count", False)), + visibility=visibility) if new_te is None: return False # In-place replace try: @@ -6741,6 +6910,13 @@ class ElementeBridge(panel_base.BaseBridge): elif t == "DUPLICATE_RAUM_STIL": # p = {id, newName?} → kopiert Stil mit neuer uuid + Name self._cmd_duplicate_raum_stil(p) + # Stempel-Stile (analog Raumstempel-Stile, eigene Storage) + elif t == "SAVE_STEMPEL_STIL": + self._cmd_save_stempel_stil(p) + elif t == "DELETE_STEMPEL_STIL": + self._cmd_delete_stempel_stil(p) + elif t == "APPLY_STEMPEL_STIL": + self._cmd_apply_stempel_stil(p) elif t == "OPEN_ELEMENTE_UEBERSICHT": try: import elemente_uebersicht @@ -6971,6 +7147,19 @@ class ElementeBridge(panel_base.BaseBridge): "font": meta.get("stempel_font", "") or "", "bold": bool(meta.get("stempel_bold", False)), "italic": bool(meta.get("stempel_italic", False)), + "header": meta.get("stempel_header", "Nutzflächen"), + "showScope": bool(meta.get("stempel_show_scope", True)), + "showHnf": bool(meta.get("stempel_show_hnf", True)), + "showNnf": bool(meta.get("stempel_show_nnf", True)), + "showNf": bool(meta.get("stempel_show_nf", True)), + "showVf": bool(meta.get("stempel_show_vf", True)), + "showFf": bool(meta.get("stempel_show_ff", True)), + "showNgf": bool(meta.get("stempel_show_ngf", True)), + "showGf": bool(meta.get("stempel_show_gf", True)), + "showAgf": bool(meta.get("stempel_show_agf", True)), + "showCount": bool(meta.get("stempel_show_count", False)), + "showSeparators": bool(meta.get("stempel_show_seps", True)), + "stilId": meta.get("stempel_stil_id", ""), "bilanz": bilanz, }) elif meta["type"] in ("stuetze_point", "traeger_axis"): @@ -7025,6 +7214,7 @@ class ElementeBridge(panel_base.BaseBridge): for n, m in _get_all_materials(doc).items()], "oeffStyles": list_oeff_styles(doc), "raumStempelStile": load_raum_stempel_stile(doc), + "stempelStile": load_stempel_stile(doc), } self.send("STATE", payload) # An Properties-Satellite-Window forwarden falls offen @@ -9054,6 +9244,101 @@ class ElementeBridge(panel_base.BaseBridge): stil.get("name"), n_applied)) self._send_state() + def _cmd_save_stempel_stil(self, p): + """Speichert einen Stempel-Stil (Layout/Header/Font-Preset). + Payload: {id?, name, settings:{header, showScope, show*, font, + bold, italic, txtH}, applyToIds?: [stempel_id, ...]}. + """ + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + name = (p.get("name") or "").strip() + if not name: + print("[ELEMENTE] SAVE_STEMPEL_STIL: name leer"); return + settings = p.get("settings") or {} + sid = (p.get("id") or "").strip() or ("stestil_" + uuid.uuid4().hex[:8]) + stile = load_stempel_stile(doc) + new_stil = {"id": sid, "name": name} + for f in _STEMPEL_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_stempel_stile(doc, stile): + print("[ELEMENTE] Stempel-Stil '{}' ({}) gespeichert".format( + name, sid)) + # Apply on listed Stempel (nur stil_id schreiben) + for eid in (p.get("applyToIds") or []): + try: + src_obj, _ = _find_source(doc, eid) + if src_obj is None: continue + attrs = src_obj.Attributes.Duplicate() + attrs.SetUserString(_KEY_STEMPEL_STIL_ID, sid) + doc.Objects.ModifyAttributes(src_obj.Id, attrs, True) + except Exception as ex: + print("[ELEMENTE] save-stempel-stil apply-to:", ex) + self._send_state() + + def _cmd_delete_stempel_stil(self, p): + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + sid = (p.get("id") or "").strip() + if not sid: return + stile = load_stempel_stile(doc) + stile = [s for s in stile if s.get("id") != sid] + if save_stempel_stile(doc, stile): + print("[ELEMENTE] Stempel-Stil {} geloescht".format(sid)) + self._send_state() + + def _cmd_apply_stempel_stil(self, p): + """Wendet einen Stempel-Stil auf Stempel-IDs an. Wenn ids leer → + alle aktuell selektierten Stempel.""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + sid = (p.get("stilId") or "").strip() + ids = p.get("ids") or [] + if not sid: return + if not ids: + for obj in doc.Objects: + try: + if obj.IsSelected(False) <= 0: continue + m = _read_meta(obj) + if m and m.get("type") == "stempel": + ids.append(m["id"]) + except Exception: pass + if not ids: return + stile = load_stempel_stile(doc) + stil = next((s for s in stile if s.get("id") == sid), None) + if stil is None: + print("[ELEMENTE] APPLY_STEMPEL_STIL: {} nicht gefunden".format(sid)) + return + # Stil-Felder → Patch fuer _update_wall stempel-Branch + patch_base = {} + for f in _STEMPEL_STIL_FIELDS: + if f in stil: patch_base[f] = stil[f] + n = 0 + for eid in ids: + try: + patch = dict(patch_base); patch["id"] = eid + self._update_wall(patch) + # stil_id markieren + try: + src_obj, _ = _find_source(doc, eid) + if src_obj is not None: + attrs = src_obj.Attributes.Duplicate() + attrs.SetUserString(_KEY_STEMPEL_STIL_ID, sid) + doc.Objects.ModifyAttributes(src_obj.Id, attrs, True) + except Exception: pass + n += 1 + except Exception as ex: + print("[ELEMENTE] apply stempel-stil {} -> {}: {}".format( + sid, eid, ex)) + if n > 0: + print("[ELEMENTE] Stempel-Stil '{}' auf {} Stempel angewendet".format( + stil.get("name"), n)) + 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. @@ -10751,7 +11036,7 @@ class ElementeBridge(panel_base.BaseBridge): doc.Views.Redraw() self._send_state() return - # Stempel: scope / txtH / font / bold / italic + # Stempel: scope / txtH / font / bold / italic + Layout-Custom if old_meta["type"] == "stempel": scope = p.get("scope", old_meta.get("stempel_scope", "total")) if not (scope == "total" or scope.startswith("geschoss:")): @@ -10761,12 +11046,47 @@ class ElementeBridge(panel_base.BaseBridge): st_font = p.get("font", old_meta.get("stempel_font", "") or "") st_bold = bool(p.get("bold", old_meta.get("stempel_bold", False))) st_ital = bool(p.get("italic", old_meta.get("stempel_italic", False))) + st_header = p.get("header", old_meta.get("stempel_header", "Nutzflächen")) + st_show_scope = bool(p.get("showScope", + old_meta.get("stempel_show_scope", True))) + st_show_hnf = bool(p.get("showHnf", + old_meta.get("stempel_show_hnf", True))) + st_show_nnf = bool(p.get("showNnf", + old_meta.get("stempel_show_nnf", True))) + st_show_nf = bool(p.get("showNf", + old_meta.get("stempel_show_nf", True))) + st_show_vf = bool(p.get("showVf", + old_meta.get("stempel_show_vf", True))) + st_show_ff = bool(p.get("showFf", + old_meta.get("stempel_show_ff", True))) + st_show_ngf = bool(p.get("showNgf", + old_meta.get("stempel_show_ngf", True))) + st_show_gf = bool(p.get("showGf", + old_meta.get("stempel_show_gf", True))) + st_show_agf = bool(p.get("showAgf", + old_meta.get("stempel_show_agf", True))) + st_show_count = bool(p.get("showCount", + old_meta.get("stempel_show_count", False))) + st_show_seps = bool(p.get("showSeparators", + old_meta.get("stempel_show_seps", True))) attrs = axis_obj.Attributes _attach_meta(attrs, wall_id, "stempel", old_meta.get("geschoss", ""), 0.0, "", "", "mid", stempel_scope=scope, stempel_txt_h=st_th, stempel_font=st_font, stempel_bold=st_bold, - stempel_italic=st_ital) + stempel_italic=st_ital, + stempel_header=st_header, + stempel_show_scope=st_show_scope, + stempel_show_hnf=st_show_hnf, + stempel_show_nnf=st_show_nnf, + stempel_show_nf=st_show_nf, + stempel_show_vf=st_show_vf, + stempel_show_ff=st_show_ff, + stempel_show_ngf=st_show_ngf, + stempel_show_gf=st_show_gf, + stempel_show_agf=st_show_agf, + stempel_show_count=st_show_count, + stempel_show_seps=st_show_seps) axis_obj.Attributes = attrs axis_obj.CommitChanges() _save_last(stempel_txt_h=st_th) @@ -12759,6 +13079,46 @@ def regen_masstab_raeume(doc=None): return len(ids) +def _sync_stempel_to_source(doc): + """Spiegelt User-Edits am Stempel-TextEntity (Font/Bold/Italic/TextHeight) + auf die UserStrings. Damit ueberleben Oberleiste-/Properties-Aenderungen + am Stempel den naechsten Regen — sonst wuerden die meta-Werte beim + Re-Render wieder gewinnen. Stempel-Source IST die TextEntity selber.""" + if doc is None: return + for obj in doc.Objects: + try: + m = _read_meta(obj) + if not m or m.get("type") != "stempel": continue + te = obj.Geometry + if not isinstance(te, rg.TextEntity): continue + try: new_h = float(te.TextHeight) + except Exception: new_h = float(m.get("stempel_txt_h", 0.20)) + cur_font = None + try: cur_font = te.Font + except Exception: pass + try: new_face = cur_font.QuartetName if cur_font else "" + except Exception: new_face = "" + try: new_bold = bool(cur_font.Bold) if cur_font else False + except Exception: new_bold = False + try: new_ital = bool(cur_font.Italic) if cur_font else False + except Exception: new_ital = False + old_h = float(m.get("stempel_txt_h", 0.20)) + old_face = m.get("stempel_font", "") or "" + old_bold = bool(m.get("stempel_bold", False)) + old_ital = bool(m.get("stempel_italic", False)) + if (abs(new_h - old_h) < 1e-6 and new_face == old_face + and new_bold == old_bold and new_ital == old_ital): + continue # nichts geaendert + attrs = obj.Attributes.Duplicate() + attrs.SetUserString(_KEY_STEMPEL_TXT_H, "{:.4f}".format(new_h)) + attrs.SetUserString(_KEY_STEMPEL_FONT, new_face) + attrs.SetUserString(_KEY_STEMPEL_BOLD, "1" if new_bold else "0") + attrs.SetUserString(_KEY_STEMPEL_ITAL, "1" if new_ital else "0") + doc.Objects.ModifyAttributes(obj.Id, attrs, True) + except Exception as ex: + print("[ELEMENTE] sync stempel:", ex) + + def _sync_raum_stamps_to_source(doc): """Sync raum_stamp TextEntities → raum_outline Source-UserStrings. @@ -12959,6 +13319,8 @@ def _on_command_end(sender, e): _was_sync = sc.sticky.get(_REGEN_BUSY, False) sc.sticky[_REGEN_BUSY] = True try: _sync_raum_stamps_to_source(doc) + except Exception: pass + try: _sync_stempel_to_source(doc) finally: sc.sticky[_REGEN_BUSY] = _was_sync except Exception as ex: print("[ELEMENTE] stamp-sync:", ex) diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx index d15e850..1d0a5a6 100644 --- a/src/ElementeApp.jsx +++ b/src/ElementeApp.jsx @@ -12,6 +12,7 @@ import { updateElement, deleteElement, openElementeUebersicht, openElementeProperties, saveOeffStyle, deleteOeffStyle, saveRaumStil, deleteRaumStil, applyRaumStil, + saveStempelStil, deleteStempelStil, applyStempelStil, listLibrary, } from './lib/rhinoBridge' @@ -512,7 +513,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, raumStempelStile }) { +export function PropertiesView({ selected, geschosse, materials, hatchPatterns, oeffStyles, fonts, raumStempelStile, stempelStile }) { if (!selected) return null const upd = (p) => updateElement(selected.id, p) const del = (label) => () => { if (window.confirm(`${label} löschen?`)) deleteElement(selected.id) } @@ -539,6 +540,7 @@ export function PropertiesView({ selected, geschosse, materials, hatchPatterns, onUpdate={upd} onDelete={del('Raum')} /> if (selected.kind === 'stempel') return if (selected.kind === 'aussparung') @@ -594,7 +596,8 @@ export default function ElementeApp() { hatchPatterns={state.hatchPatterns} fonts={state.fonts || []} oeffStyles={state.oeffStyles || []} - raumStempelStile={state.raumStempelStile || []} /> + raumStempelStile={state.raumStempelStile || []} + stempelStile={state.stempelStile || []} /> )} { + setHeaderDraft(stempel.header || 'Nutzflächen') + }, [stempel.id, stempel.header]) + + // Show-Flags vom Backend (default true ausser showCount) + const _show = (k, def = true) => + (stempel[k] !== undefined ? !!stempel[k] : def) + + const handleStilChange = (val) => { + if (val === '__save__') { + const n = (window.prompt('Name für neuen Stempel-Stil:', 'Stil') || '').trim() + if (!n) return + saveStempelStil('', n, { + header: stempel.header || 'Nutzflächen', + showScope: _show('showScope'), + showHnf: _show('showHnf'), + showNnf: _show('showNnf'), + showNf: _show('showNf'), + showVf: _show('showVf'), + showFf: _show('showFf'), + showNgf: _show('showNgf'), + showGf: _show('showGf'), + showAgf: _show('showAgf'), + showCount: _show('showCount', false), + showSeparators: _show('showSeparators'), + font: stempel.font || '', + bold: !!stempel.bold, + italic: !!stempel.italic, + txtH: stempel.txtH, + }, [stempel.id]) + return + } + if (val === '__delete__') { + if (activeStilId && + window.confirm(`Stil "${stilList.find(s => s.id === activeStilId)?.name}" löschen?`)) + deleteStempelStil(activeStilId) + return + } + // Empty ids → Backend nutzt aktuell selektierte Stempel + if (val) applyStempelStil(val, []) + } + + // Sichtbare Zeilen fuer die Live-Vorschau (nur die mit Werten + Visibility) const rows = [ - { key: 'hnf', label: 'HNF', val: bilanz.hnf }, - { key: 'nnf', label: 'NNF', val: bilanz.nnf }, - { key: 'nf', label: 'NF', val: bilanz.nf, accent: true, + { key: 'hnf', label: 'HNF', val: bilanz.hnf, on: _show('showHnf') }, + { key: 'nnf', label: 'NNF', val: bilanz.nnf, on: _show('showNnf') }, + { key: 'nf', label: 'NF', val: bilanz.nf, on: _show('showNf'), accent: true, hint: 'Nutzfläche = HNF + NNF' }, - { key: 'vf', label: 'VF', val: bilanz.vf }, - { key: 'ff', label: 'FF', val: bilanz.ff }, - { key: 'ngf', label: 'NGF', val: bilanz.ngf, + { key: 'vf', label: 'VF', val: bilanz.vf, on: _show('showVf') }, + { key: 'ff', label: 'FF', val: bilanz.ff, on: _show('showFf') }, + { key: 'ngf', label: 'NGF', val: bilanz.ngf, on: _show('showNgf'), hint: 'Nettogeschossfläche = NF + VF + FF' }, - { key: 'gf', label: 'GF', val: bilanz.gf }, - { key: 'agf', label: 'AGF', val: bilanz.agf }, - ].filter(r => r.val && r.val > 0) + { key: 'gf', label: 'GF', val: bilanz.gf, on: _show('showGf') }, + { key: 'agf', label: 'AGF', val: bilanz.agf, on: _show('showAgf') }, + ].filter(r => r.on && r.val && r.val > 0) + + const ShowTog = ({ field, label, hint }) => ( + onUpdate({ [field]: !_show(field, field === 'showCount' ? false : true) })} + title={hint || label} /> + ) + return (
+ {/* Stil-Picker — analog Raumstempel-Stile */} +
+ Stil + +
+
Scope setHeaderDraft(e.target.value)} + onBlur={() => { + const v = (headerDraft || '').trim() + if (v !== (stempel.header || '')) onUpdate({ header: v }) + }} + onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} + placeholder="Nutzflächen" + style={{ flex: 1, fontSize: 11 }} /> +
+ + {/* Show-Toggles — was im Stempel angezeigt wird */} +
+ Anzeigen im Stempel +
+ + + + + + + + + + + +
+
+ + {/* Live-Vorschau der Bilanz (nur sichtbare Zeilen) */}
- Bilanz · {bilanz.count || 0} Räume klassifiziert + Vorschau · {bilanz.count || 0} Räume klassifiziert
{rows.length === 0 ? (
- Keine klassifizierten Räume im Scope. - Räume mit SIA-Tag (HNF/NNF/VF/FF/GF/AGF) erscheinen hier. + Nichts anzuzeigen — Räume mit SIA-Tag fehlen oder Felder deaktiviert.
) : rows.map(r => (
) : (