Stempel: Stile + Layout-Customisation (Header, Show-Toggles)

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
This commit is contained in:
2026-05-27 00:36:31 +02:00
parent ac7b2f2ee5
commit 975071c995
4 changed files with 528 additions and 46 deletions
+393 -31
View File
@@ -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, raum_layout=None, raum_txt_modus=None,
stempel_scope=None, stempel_txt_h=None, stempel_scope=None, stempel_txt_h=None,
stempel_font=None, stempel_bold=None, stempel_italic=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_layered=None, wand_layers=None, wand_layer_idx=None,
wand_chain_members=None, wand_chain_members=None,
aussp_parent=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: if stempel_italic is not None:
obj_attrs.SetUserString(_KEY_STEMPEL_ITAL, obj_attrs.SetUserString(_KEY_STEMPEL_ITAL,
"1" if bool(stempel_italic) else "0") "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 # Wand-Schichten
if wand_layered is not None: if wand_layered is not None:
obj_attrs.SetUserString(_KEY_WAND_LAYERED, obj_attrs.SetUserString(_KEY_WAND_LAYERED,
@@ -3141,6 +3167,29 @@ def _read_meta(obj):
st_font = a.GetUserString(_KEY_STEMPEL_FONT) or "" st_font = a.GetUserString(_KEY_STEMPEL_FONT) or ""
st_bold = (a.GetUserString(_KEY_STEMPEL_BOLD) == "1") st_bold = (a.GetUserString(_KEY_STEMPEL_BOLD) == "1")
st_ital = (a.GetUserString(_KEY_STEMPEL_ITAL) == "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 # Field-Layout — parsed JSON list of rows
r_layout_raw = a.GetUserString(_KEY_RAUM_LAYOUT) or "" r_layout_raw = a.GetUserString(_KEY_RAUM_LAYOUT) or ""
r_layout = [] r_layout = []
@@ -3268,6 +3317,19 @@ def _read_meta(obj):
"stempel_font": st_font, "stempel_font": st_font,
"stempel_bold": st_bold, "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,
"stempel_stil_id": st_stil_id,
"wand_layered": w_layered, "wand_layered": w_layered,
"wand_layers": w_layers, "wand_layers": w_layers,
"wand_layer_idx": w_layer_idx, "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_FONT = "dossier_stempel_font"
_KEY_STEMPEL_BOLD = "dossier_stempel_bold" _KEY_STEMPEL_BOLD = "dossier_stempel_bold"
_KEY_STEMPEL_ITAL = "dossier_stempel_italic" _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 # Oeffnungs-Cutout: Boolean-Difference aus Wand. Zusaetzlich kriegt die
# Oeffnung ihr eigenes Volumen (Rahmen + Sims + Glas) als Sub-Element. # Oeffnung ihr eigenes Volumen (Rahmen + Sims + Glas) als Sub-Element.
@@ -5061,51 +5175,89 @@ def compute_sia_bilanz(doc, scope="total"):
return out 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 """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 = [] lines = []
def _row(label, val): def _row(label, val):
return "{} {}".format(label.ljust(4), _format_area(val, rundung)) return "{} {}".format(label.ljust(4), _format_area(val, rundung))
# Nutzflaechen-Block # Nutzflaechen-Block
has_nf = bilanz["hnf"] > 0 or bilanz["nnf"] > 0 show_hnf = _v("hnf") and bilanz["hnf"] > 0
if bilanz["hnf"] > 0: lines.append(_row("HNF", bilanz["hnf"])) show_nnf = _v("nnf") and bilanz["nnf"] > 0
if bilanz["nnf"] > 0: lines.append(_row("NNF", bilanz["nnf"])) has_nf_inputs = show_hnf or show_nnf
if has_nf: if show_hnf: lines.append(_row("HNF", bilanz["hnf"]))
lines.append("────────────") 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"])) lines.append(_row("NF", bilanz["nf"]))
# VF/FF # VF/FF
if bilanz["vf"] > 0: lines.append(_row("VF", bilanz["vf"])) show_vf = _v("vf") and bilanz["vf"] > 0
if bilanz["ff"] > 0: lines.append(_row("FF", bilanz["ff"])) 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 # NGF wenn was zusammenkommt
if bilanz["ngf"] > 0 and (bilanz["vf"] > 0 or bilanz["ff"] > 0): if _v("ngf") and bilanz["ngf"] > 0 and (show_vf or show_ff or has_nf_inputs):
lines.append("────────────") if show_separators: lines.append(sep)
lines.append(_row("NGF", bilanz["ngf"])) lines.append(_row("NGF", bilanz["ngf"]))
# Gross-Flaechen separat # Gross-Flaechen separat
if bilanz["gf"] > 0 or bilanz["agf"] > 0: show_gf = _v("gf") and bilanz["gf"] > 0
lines.append("────────────") show_agf = _v("agf") and bilanz["agf"] > 0
if bilanz["gf"] > 0: lines.append(_row("GF", bilanz["gf"])) if show_gf or show_agf:
if bilanz["agf"] > 0: lines.append(_row("AGF", bilanz["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 return lines
def _make_stempel_text(pos, scope, doc, text_height=0.20, z=0.0, 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 """Baut die Stempel-TextEntity fuer einen Scope ("total" oder
"geschoss:<id>"). Header = "Nutzflächen · {Scope-Label}".""" "geschoss:<id>"). 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: try:
bilanz = compute_sia_bilanz(doc, scope) bilanz = compute_sia_bilanz(doc, scope)
if scope == "total": # Header zusammenbauen
scope_label = "Total" header_parts = []
elif bilanz["geschossName"]: if header_text and header_text.strip():
scope_label = bilanz["geschossName"] header_parts.append(header_text.strip())
else: if show_scope:
scope_label = "" if scope == "total":
header = "Nutzflächen · {}".format(scope_label) scope_label = "Total"
body = _format_bilanz_lines(bilanz) 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: if not body:
body = ["(keine klassifizierten Räume)"] 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 = rg.TextEntity()
te.Text = text te.Text = text
plane = rg.Plane(rg.Point3d(pos.X, pos.Y, float(z)), 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 "" font = meta.get("stempel_font", "") or ""
bold = bool(meta.get("stempel_bold", False)) bold = bool(meta.get("stempel_bold", False))
italic = bool(meta.get("stempel_italic", False)) italic = bool(meta.get("stempel_italic", False))
new_te = _make_stempel_text(pos, scope, doc, text_height=txt_h, # Layout-Customisation aus Meta
z=pos.Z, font=font, bold=bold, header_text = meta.get("stempel_header", "Nutzflächen") or "Nutzflächen"
italic=italic) 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 if new_te is None: return False
# In-place replace # In-place replace
try: try:
@@ -6741,6 +6910,13 @@ class ElementeBridge(panel_base.BaseBridge):
elif t == "DUPLICATE_RAUM_STIL": elif t == "DUPLICATE_RAUM_STIL":
# p = {id, newName?} → kopiert Stil mit neuer uuid + Name # p = {id, newName?} → kopiert Stil mit neuer uuid + Name
self._cmd_duplicate_raum_stil(p) 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": elif t == "OPEN_ELEMENTE_UEBERSICHT":
try: try:
import elemente_uebersicht import elemente_uebersicht
@@ -6971,6 +7147,19 @@ class ElementeBridge(panel_base.BaseBridge):
"font": meta.get("stempel_font", "") or "", "font": meta.get("stempel_font", "") or "",
"bold": bool(meta.get("stempel_bold", False)), "bold": bool(meta.get("stempel_bold", False)),
"italic": bool(meta.get("stempel_italic", 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, "bilanz": bilanz,
}) })
elif meta["type"] in ("stuetze_point", "traeger_axis"): 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()], for n, m in _get_all_materials(doc).items()],
"oeffStyles": list_oeff_styles(doc), "oeffStyles": list_oeff_styles(doc),
"raumStempelStile": load_raum_stempel_stile(doc), "raumStempelStile": load_raum_stempel_stile(doc),
"stempelStile": load_stempel_stile(doc),
} }
self.send("STATE", payload) self.send("STATE", payload)
# An Properties-Satellite-Window forwarden falls offen # An Properties-Satellite-Window forwarden falls offen
@@ -9054,6 +9244,101 @@ class ElementeBridge(panel_base.BaseBridge):
stil.get("name"), n_applied)) stil.get("name"), n_applied))
self._send_state() 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): def _cmd_create_symbol(self, p):
"""Platziert ein Library-Item (symbol/object) im Doc. Interactive """Platziert ein Library-Item (symbol/object) im Doc. Interactive
GetPoint im aktiven Viewport User klickt Position. GetPoint im aktiven Viewport User klickt Position.
@@ -10751,7 +11036,7 @@ class ElementeBridge(panel_base.BaseBridge):
doc.Views.Redraw() doc.Views.Redraw()
self._send_state() self._send_state()
return return
# Stempel: scope / txtH / font / bold / italic # Stempel: scope / txtH / font / bold / italic + Layout-Custom
if old_meta["type"] == "stempel": if old_meta["type"] == "stempel":
scope = p.get("scope", old_meta.get("stempel_scope", "total")) scope = p.get("scope", old_meta.get("stempel_scope", "total"))
if not (scope == "total" or scope.startswith("geschoss:")): 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_font = p.get("font", old_meta.get("stempel_font", "") or "")
st_bold = bool(p.get("bold", old_meta.get("stempel_bold", False))) 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_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 attrs = axis_obj.Attributes
_attach_meta(attrs, wall_id, "stempel", _attach_meta(attrs, wall_id, "stempel",
old_meta.get("geschoss", ""), 0.0, "", "", "mid", old_meta.get("geschoss", ""), 0.0, "", "", "mid",
stempel_scope=scope, stempel_txt_h=st_th, stempel_scope=scope, stempel_txt_h=st_th,
stempel_font=st_font, stempel_bold=st_bold, 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.Attributes = attrs
axis_obj.CommitChanges() axis_obj.CommitChanges()
_save_last(stempel_txt_h=st_th) _save_last(stempel_txt_h=st_th)
@@ -12759,6 +13079,46 @@ def regen_masstab_raeume(doc=None):
return len(ids) 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): def _sync_raum_stamps_to_source(doc):
"""Sync raum_stamp TextEntities → raum_outline Source-UserStrings. """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) _was_sync = sc.sticky.get(_REGEN_BUSY, False)
sc.sticky[_REGEN_BUSY] = True sc.sticky[_REGEN_BUSY] = True
try: _sync_raum_stamps_to_source(doc) try: _sync_raum_stamps_to_source(doc)
except Exception: pass
try: _sync_stempel_to_source(doc)
finally: sc.sticky[_REGEN_BUSY] = _was_sync finally: sc.sticky[_REGEN_BUSY] = _was_sync
except Exception as ex: except Exception as ex:
print("[ELEMENTE] stamp-sync:", ex) print("[ELEMENTE] stamp-sync:", ex)
+125 -15
View File
@@ -12,6 +12,7 @@ import {
updateElement, deleteElement, openElementeUebersicht, openElementeProperties, updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
saveOeffStyle, deleteOeffStyle, saveOeffStyle, deleteOeffStyle,
saveRaumStil, deleteRaumStil, applyRaumStil, saveRaumStil, deleteRaumStil, applyRaumStil,
saveStempelStil, deleteStempelStil, applyStempelStil,
listLibrary, listLibrary,
} from './lib/rhinoBridge' } from './lib/rhinoBridge'
@@ -512,7 +513,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
// PropertiesView: gemeinsame Komponente, rendert die passende Property- // PropertiesView: gemeinsame Komponente, rendert die passende Property-
// Form je nach Element-Typ. Wiederverwendbar in Inline + Satellite-Window. // 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 if (!selected) return null
const upd = (p) => updateElement(selected.id, p) const upd = (p) => updateElement(selected.id, p)
const del = (label) => () => { if (window.confirm(`${label} löschen?`)) deleteElement(selected.id) } 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')} /> onUpdate={upd} onDelete={del('Raum')} />
if (selected.kind === 'stempel') if (selected.kind === 'stempel')
return <StempelProperties stempel={selected} geschosse={geschosse} return <StempelProperties stempel={selected} geschosse={geschosse}
stempelStile={stempelStile || []}
onUpdate={upd} onDelete={del('Stempel')} /> onUpdate={upd} onDelete={del('Stempel')} />
if (selected.kind === 'aussparung') if (selected.kind === 'aussparung')
@@ -594,7 +596,8 @@ export default function ElementeApp() {
hatchPatterns={state.hatchPatterns} hatchPatterns={state.hatchPatterns}
fonts={state.fonts || []} fonts={state.fonts || []}
oeffStyles={state.oeffStyles || []} oeffStyles={state.oeffStyles || []}
raumStempelStile={state.raumStempelStile || []} /> raumStempelStile={state.raumStempelStile || []}
stempelStile={state.stempelStile || []} />
</div> </div>
)} )}
<NeuesElementSection <NeuesElementSection
@@ -1168,21 +1171,76 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
) )
} }
function StempelProperties({ stempel, geschosse, onUpdate, onDelete }) { function StempelProperties({ stempel, geschosse, stempelStile, onUpdate, onDelete }) {
const scope = stempel.scope || 'total' const scope = stempel.scope || 'total'
const bilanz = stempel.bilanz || {} const bilanz = stempel.bilanz || {}
const stilList = stempelStile || []
const activeStilId = stempel.stilId || ''
const [headerDraft, setHeaderDraft] = useState(stempel.header || 'Nutzflächen')
useEffect(() => {
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 = [ const rows = [
{ key: 'hnf', label: 'HNF', val: bilanz.hnf }, { key: 'hnf', label: 'HNF', val: bilanz.hnf, on: _show('showHnf') },
{ key: 'nnf', label: 'NNF', val: bilanz.nnf }, { key: 'nnf', label: 'NNF', val: bilanz.nnf, on: _show('showNnf') },
{ key: 'nf', label: 'NF', val: bilanz.nf, accent: true, { key: 'nf', label: 'NF', val: bilanz.nf, on: _show('showNf'), accent: true,
hint: 'Nutzfläche = HNF + NNF' }, hint: 'Nutzfläche = HNF + NNF' },
{ key: 'vf', label: 'VF', val: bilanz.vf }, { key: 'vf', label: 'VF', val: bilanz.vf, on: _show('showVf') },
{ key: 'ff', label: 'FF', val: bilanz.ff }, { key: 'ff', label: 'FF', val: bilanz.ff, on: _show('showFf') },
{ key: 'ngf', label: 'NGF', val: bilanz.ngf, { key: 'ngf', label: 'NGF', val: bilanz.ngf, on: _show('showNgf'),
hint: 'Nettogeschossfläche = NF + VF + FF' }, hint: 'Nettogeschossfläche = NF + VF + FF' },
{ key: 'gf', label: 'GF', val: bilanz.gf }, { key: 'gf', label: 'GF', val: bilanz.gf, on: _show('showGf') },
{ key: 'agf', label: 'AGF', val: bilanz.agf }, { key: 'agf', label: 'AGF', val: bilanz.agf, on: _show('showAgf') },
].filter(r => r.val && r.val > 0) ].filter(r => r.on && r.val && r.val > 0)
const ShowTog = ({ field, label, hint }) => (
<BarToggle label={label}
icon={_show(field, field === 'showCount' ? false : true) ? 'check_box' : 'check_box_outline_blank'}
active={_show(field, field === 'showCount' ? false : true)}
onClick={() => onUpdate({ [field]: !_show(field, field === 'showCount' ? false : true) })}
title={hint || label} />
)
return ( return (
<div style={{ <div style={{
display: 'flex', flexDirection: 'column', gap: 8, display: 'flex', flexDirection: 'column', gap: 8,
@@ -1202,6 +1260,23 @@ function StempelProperties({ stempel, geschosse, onUpdate, onDelete }) {
</button> </button>
</div> </div>
{/* Stil-Picker — analog Raumstempel-Stile */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Stil</span>
<select value={activeStilId}
onChange={(e) => handleStilChange(e.target.value)}
style={{ flex: 1, fontSize: 11 }}
title="Gespeicherter Stempel-Stil — Klick wendet die Vorlage an">
<option value=""> Stil wählen </option>
{stilList.map(s => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
<option disabled></option>
<option value="__save__">+ Aktuelle Settings als Stil speichern</option>
{activeStilId && <option value="__delete__">🗑 Aktiven Stil löschen</option>}
</select>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Scope</span> <span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Scope</span>
<select value={scope} <select value={scope}
@@ -1217,6 +1292,42 @@ function StempelProperties({ stempel, geschosse, onUpdate, onDelete }) {
</select> </select>
</div> </div>
{/* Header-Input — frei editierbar, Default "Nutzflächen" */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Header</span>
<input type="text" value={headerDraft}
onChange={(e) => 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 }} />
</div>
{/* Show-Toggles — was im Stempel angezeigt wird */}
<div style={{
display: 'flex', flexDirection: 'column', gap: 4,
paddingTop: 6, borderTop: '1px dashed var(--border)',
}}>
<span style={{ ...labelXs, marginBottom: 2 }}>Anzeigen im Stempel</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
<ShowTog field="showScope" label="Scope" hint="Header-Suffix · Total/Geschoss" />
<ShowTog field="showHnf" label="HNF" />
<ShowTog field="showNnf" label="NNF" />
<ShowTog field="showNf" label="NF" hint="Nutzfläche = HNF + NNF" />
<ShowTog field="showVf" label="VF" />
<ShowTog field="showFf" label="FF" />
<ShowTog field="showNgf" label="NGF" hint="Nettogeschossfläche" />
<ShowTog field="showGf" label="GF" />
<ShowTog field="showAgf" label="AGF" />
<ShowTog field="showCount" label="Anzahl" hint="Anzahl klassifizierter Räume" />
<ShowTog field="showSeparators" label="Linien" hint="Trennlinien zwischen Sections" />
</div>
</div>
{/* Live-Vorschau der Bilanz (nur sichtbare Zeilen) */}
<div style={{ <div style={{
paddingTop: 6, borderTop: '1px dashed var(--border)', paddingTop: 6, borderTop: '1px dashed var(--border)',
fontSize: 10, fontFamily: 'DM Mono, monospace', fontSize: 10, fontFamily: 'DM Mono, monospace',
@@ -1225,13 +1336,12 @@ function StempelProperties({ stempel, geschosse, onUpdate, onDelete }) {
<div style={{ fontSize: 9, color: 'var(--text-muted)', <div style={{ fontSize: 9, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase', letterSpacing: '0.06em', textTransform: 'uppercase',
marginBottom: 4 }}> marginBottom: 4 }}>
Bilanz · {bilanz.count || 0} Räume klassifiziert Vorschau · {bilanz.count || 0} Räume klassifiziert
</div> </div>
{rows.length === 0 ? ( {rows.length === 0 ? (
<div style={{ fontSize: 10, color: 'var(--text-muted)', <div style={{ fontSize: 10, color: 'var(--text-muted)',
fontStyle: 'italic', padding: '4px 0' }}> fontStyle: 'italic', padding: '4px 0' }}>
Keine klassifizierten Räume im Scope. Nichts anzuzeigen Räume mit SIA-Tag fehlen oder Felder deaktiviert.
Räume mit SIA-Tag (HNF/NNF/VF/FF/GF/AGF) erscheinen hier.
</div> </div>
) : rows.map(r => ( ) : rows.map(r => (
<div key={r.key} title={r.hint || ''} <div key={r.key} title={r.hint || ''}
+1
View File
@@ -38,6 +38,7 @@ export default function ElementePropertiesApp() {
fonts={state.fonts || []} fonts={state.fonts || []}
oeffStyles={state.oeffStyles || []} oeffStyles={state.oeffStyles || []}
raumStempelStile={state.raumStempelStile || []} raumStempelStile={state.raumStempelStile || []}
stempelStile={state.stempelStile || []}
/> />
) : ( ) : (
<div style={{ <div style={{
+9
View File
@@ -256,6 +256,15 @@ export function reorderRaumStile(ids) { send('REORDER_RAUM_STILE', { ids }) }
export function duplicateRaumStil(id, newName) { export function duplicateRaumStil(id, newName) {
send('DUPLICATE_RAUM_STIL', { id, newName }) send('DUPLICATE_RAUM_STIL', { id, newName })
} }
// Stempel-Stile (analog Raumstempel-Stile, eigene Storage pro Doc)
export function saveStempelStil(id, name, settings, applyToIds) {
send('SAVE_STEMPEL_STIL', { id, name, settings,
applyToIds: applyToIds || [] })
}
export function deleteStempelStil(id) { send('DELETE_STEMPEL_STIL', { id }) }
export function applyStempelStil(stilId, ids) {
send('APPLY_STEMPEL_STIL', { stilId, ids })
}
export function setSectionStyle(enabled, source, color, pattern, scale, rotation, export function setSectionStyle(enabled, source, color, pattern, scale, rotation,
opts = {}) { opts = {}) {
send('SET_SECTION_STYLE', { send('SET_SECTION_STYLE', {