From 23863665660a8571822ee2a078cbbf5e01b730f9 Mon Sep 17 00:00:00 2001 From: karim Date: Wed, 27 May 2026 00:10:02 +0200 Subject: [PATCH] Stempel-Element: SIA-Bilanz als platzierbares Viewport-Objekt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer Element-Type "stempel" — TextEntity die automatisch eine SIA-416 Bilanz aggregiert und im Plan platziert wird. Re-rendert sich live wenn sich Raeume im Scope aendern. Backend (elemente.py): - Neue SIA-Tags: GF (Geschossflaeche), AGF (Aussengeschossflaeche) mit eigenen Labels + Pastell-Farben in _SIA_COLORS_HEX - "stempel" als SOURCE_TYPE; eigene UserStrings: - stempel_scope: "total" | "geschoss:" - stempel_txt_h, stempel_font, stempel_bold, stempel_italic - compute_sia_bilanz(doc, scope): aggregiert nach SIA-Tags, liefert HNF/NNF/VF/FF/GF/AGF + abgeleitet NF/NGF/count + Scope-Label - _format_bilanz_lines: kompakte Stempel-Textzeilen ("HNF 120.5 m²"), Trennlinien + nur Kategorien > 0 - _make_stempel_text: TextEntity-Builder mit Header "Nutzflächen · {Scope}" - _regenerate_element_body "stempel"-Branch: in-place Replace mit aktualisiertem Text (Position bleibt aus alter Geometrie) - _regenerate_stempel_for_geschoss: regennt alle Stempel im selben Geschoss + alle "total"-Stempel - Auto-Cascade: raum_outline-Regen setzt sticky-marker; nach REGEN_BUSY- Release wird _regenerate_stempel_for_geschoss aufgerufen - _cmd_create_stempel: GetPoint im Viewport, layer = aktives Geschoss Raum-Sublayer, default-Scope = aktives Geschoss - _update_wall "stempel"-Branch: scope/txtH/font/bold/italic via patch - elemente_uebersicht: SIA-Bilanz um gf/agf/ngf erweitert; "stempel" als KIND-Eintrag Frontend: - createStempel-Bridge-Export - "Stempel"-Pill-Button in der "Raeume"-PillGroup - StempelProperties-Component: Scope-Dropdown (Total + alle Geschosse), Bilanz-Vorschau mit Hervorhebung der NF (Accent-Farbe) - KIND_META + RAUM_SIA_KINDS um GF/AGF erweitert Workflow: Pill "Stempel" klicken → Punkt im Viewport → Stempel erscheint mit Header "Nutzflächen · EG" + Bilanz. Properties: Scope auf Total umstellen oder anderes Geschoss waehlen. Neue Raeume taggen mit SIA- Tag → Stempel aktualisiert sich automatisch. --- rhino/elemente.py | 331 +++++++++++++++++++++++++++++++++- rhino/elemente_uebersicht.py | 10 +- src/ElementeApp.jsx | 104 ++++++++++- src/ElementeUebersichtApp.jsx | 3 +- src/lib/rhinoBridge.js | 1 + 5 files changed, 440 insertions(+), 9 deletions(-) diff --git a/rhino/elemente.py b/rhino/elemente.py index 70ee3f5..b774575 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -408,18 +408,23 @@ def _resolve_raum_rundung(meta, doc=None): _RAUM_ALIGN = ("links", "mid", "rechts") -_RAUM_SIA_KINDS = ("", "hnf", "nnf", "vf", "ff") +_RAUM_SIA_KINDS = ("", "hnf", "nnf", "vf", "ff", "gf", "agf") _RAUM_FUNKTIONEN = ( "wohnen", "schlafen", "bad", "kueche", "essen", "flur", "diele", "buero", "atelier", "lager", "technik", "balkon", "terrasse", "sonstiges", ) # SIA-416 Farbpalette nach CH-Buero-Konvention (helle, kraeftige Pastelltoene). +# GF/AGF: dezente Grau-Toene weil sie meist als Hintergrund-Outline um andere +# klassifizierte Raeume gezeichnet werden (Doppelung wird vom Stempel +# erkannt: GF/AGF separat aggregiert). _SIA_COLORS_HEX = { "hnf": "#e8a8a8", # Hauptnutzflaeche — Rot "nnf": "#e8c498", # Nebennutzflaeche — Orange "vf": "#e8d878", # Verkehrsflaeche — Gelb "ff": "#a8c8e0", # Funktionsflaeche — Hellblau + "gf": "#d0d0d0", # Geschossflaeche (gross) — Grau + "agf": "#c0d8c0", # Aussengeschossflaeche — Hellgruen } _SIA_LABELS = { "": "—", @@ -427,6 +432,8 @@ _SIA_LABELS = { "nnf": "NNF", "vf": "VF", "ff": "FF", + "gf": "GF", + "agf": "AGF", } # Cross-Doc Preset-Name fuer Override-Engine. Steuert auch das siaFillMode- @@ -2676,6 +2683,8 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, raum_show_sia=None, raum_stamp_dx=None, raum_stamp_dy=None, raum_layout=None, raum_txt_modus=None, + stempel_scope=None, stempel_txt_h=None, + stempel_font=None, stempel_bold=None, stempel_italic=None, wand_layered=None, wand_layers=None, wand_layer_idx=None, wand_chain_members=None, aussp_parent=None): @@ -2880,6 +2889,21 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, print("[ELEMENTE] raum_layout set:", ex) if raum_txt_modus is not None and raum_txt_modus in ("fix", "masstab"): obj_attrs.SetUserString(_KEY_RAUM_TXT_MODUS, raum_txt_modus) + # Stempel-Felder + if stempel_scope is not None: + obj_attrs.SetUserString(_KEY_STEMPEL_SCOPE, str(stempel_scope)) + if stempel_txt_h is not None: + try: obj_attrs.SetUserString(_KEY_STEMPEL_TXT_H, + "{:.4f}".format(float(stempel_txt_h))) + except Exception: pass + if stempel_font is not None: + obj_attrs.SetUserString(_KEY_STEMPEL_FONT, str(stempel_font)) + if stempel_bold is not None: + obj_attrs.SetUserString(_KEY_STEMPEL_BOLD, + "1" if bool(stempel_bold) else "0") + if stempel_italic is not None: + obj_attrs.SetUserString(_KEY_STEMPEL_ITAL, + "1" if bool(stempel_italic) else "0") # Wand-Schichten if wand_layered is not None: obj_attrs.SetUserString(_KEY_WAND_LAYERED, @@ -3080,6 +3104,13 @@ def _read_meta(obj): if r_txt_modus not in ("fix", "masstab"): r_txt_modus = "fix" # Aktiver Stempel-Stil (id des zuletzt applizierten Stils) r_stil_id = a.GetUserString(_KEY_RAUM_STIL_ID) or "" + # Stempel-Felder + st_scope = a.GetUserString(_KEY_STEMPEL_SCOPE) or "total" + try: st_th = float(a.GetUserString(_KEY_STEMPEL_TXT_H) or "0.20") + except Exception: st_th = 0.20 + st_font = a.GetUserString(_KEY_STEMPEL_FONT) or "" + st_bold = (a.GetUserString(_KEY_STEMPEL_BOLD) == "1") + st_ital = (a.GetUserString(_KEY_STEMPEL_ITAL) == "1") # Field-Layout — parsed JSON list of rows r_layout_raw = a.GetUserString(_KEY_RAUM_LAYOUT) or "" r_layout = [] @@ -3202,6 +3233,11 @@ def _read_meta(obj): "raum_layout": r_layout, "raum_txt_modus": r_txt_modus, "raum_stil_id": r_stil_id, + "stempel_scope": st_scope, + "stempel_txt_h": st_th, + "stempel_font": st_font, + "stempel_bold": st_bold, + "stempel_italic": st_ital, "wand_layered": w_layered, "wand_layers": w_layers, "wand_layer_idx": w_layer_idx, @@ -3808,11 +3844,24 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base SOURCE_TYPES = ("wand_axis", "decke_outline", "dach_outline", "oeffnung_point", "treppe_axis", "stuetze_point", "traeger_axis", - "raum_outline", "decke_aussparung_outline") + "raum_outline", "decke_aussparung_outline", + # Stempel: SIA-Bilanz-TextEntity. Source-only (kein Volume), + # die TextEntity selber IST die Source-Geometrie. Regennt + # automatisch wenn raeume im Scope sich aendern. + "stempel") VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume", "oeffnung_volume", "oeffnung_swing", "oeffnung_sturz", "treppe_volume", "stuetze_volume", "traeger_volume", "raum_stamp", "raum_fill") + +# Stempel-Scope-Werte: +# "total" → alle Raeume im Doc +# "geschoss:" → nur Raeume im genannten Geschoss +_KEY_STEMPEL_SCOPE = "dossier_stempel_scope" +_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" # Oeffnungs-Cutout: Boolean-Difference aus Wand. Zusaetzlich kriegt die # Oeffnung ihr eigenes Volumen (Rahmen + Sims + Glas) als Sub-Element. @@ -4938,6 +4987,144 @@ def _make_raum_stamp_text(centroid, name, nummer, funktion, area, rundung, return None +def compute_sia_bilanz(doc, scope="total"): + """Aggregiert raum_outline-Flaechen nach raum_sia-Klassifikation. + scope = "total" → alle Raeume + scope = "geschoss:" → nur Raeume mit raum_geschoss == id + Liefert dict {hnf, nnf, vf, ff, gf, agf, nf, ngf, count, scope, geschossName}. + NF = HNF + NNF (Nutzflaeche). NGF = NF + VF + FF (Nettogeschossflaeche + laut SIA 416). + """ + out = {"hnf": 0.0, "nnf": 0.0, "vf": 0.0, "ff": 0.0, + "gf": 0.0, "agf": 0.0, "count": 0, + "scope": scope, "geschossName": ""} + if doc is None: return out + target_gid = None + if isinstance(scope, str) and scope.startswith("geschoss:"): + target_gid = scope.split(":", 1)[1] + g = _geschoss_by_id(doc, target_gid) + if g: out["geschossName"] = g.get("name") or "" + for obj in doc.Objects: + try: + m = _read_meta(obj) + if not m or m.get("type") != "raum_outline": continue + if target_gid is not None and m.get("geschoss") != target_gid: + continue + try: area, _, _ = _raum_amp(obj.Geometry) + except Exception: continue + if not area or area <= 0: continue + sia = (m.get("raum_sia") or "").lower() + if sia not in ("hnf", "nnf", "vf", "ff", "gf", "agf"): continue + out[sia] += float(area) + out["count"] += 1 + except Exception: pass + out["nf"] = out["hnf"] + out["nnf"] + out["ngf"] = out["nf"] + out["vf"] + out["ff"] + return out + + +def _format_bilanz_lines(bilanz, rundung="0.1"): + """Baut die Stempel-Textzeilen aus einer Bilanz. Zeigt nur Kategorien + mit Flaeche > 0. Format kompakt: 'HNF 120.5 m²'.""" + 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("────────────") + 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"])) + # NGF wenn was zusammenkommt + if bilanz["ngf"] > 0 and (bilanz["vf"] > 0 or bilanz["ff"] > 0): + lines.append("────────────") + 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"])) + return lines + + +def _make_stempel_text(pos, scope, doc, text_height=0.20, z=0.0, + font=None, bold=False, italic=False): + """Baut die Stempel-TextEntity fuer einen Scope ("total" oder + "geschoss:"). Header = "Nutzflächen · {Scope-Label}".""" + 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) + if not body: + body = ["(keine klassifizierten Räume)"] + text = "\n".join([header, "════════════"] + body) + te = rg.TextEntity() + te.Text = text + plane = rg.Plane(rg.Point3d(pos.X, pos.Y, float(z)), + rg.Vector3d.ZAxis) + te.Plane = plane + try: te.TextHeight = float(text_height) + except Exception: te.TextHeight = 0.20 + try: + te.Justification = rg.TextJustification.MiddleLeft + except Exception: pass + # Font/Style optional + if doc is not None and font: + try: + f = Rhino.DocObjects.Font.FromQuartetProperties( + str(font), bool(bold), bool(italic)) + if f is not None: + idx = doc.Fonts.FindOrCreate(f.QuartetName, + bool(bold), bool(italic)) + if idx >= 0: + try: te.FontIndex = idx + except Exception: pass + except Exception: pass + return te + except Exception as ex: + print("[ELEMENTE] _make_stempel_text:", ex) + return None + + +def _regenerate_stempel_for_geschoss(doc, geschoss_id): + """Regennt alle Stempel die diesen Geschoss-Scope (oder 'total') haben. + Wird nach jedem raum_outline-Regen aufgerufen damit Bilanzen aktuell + bleiben. Idempotent + safe gegen Endless-Loops via _REGEN_BUSY-Check.""" + if doc is None: return + if sc.sticky.get(_REGEN_BUSY): return + target_scopes = {"total"} + if geschoss_id: + target_scopes.add("geschoss:{}".format(geschoss_id)) + ids = [] + for obj in doc.Objects: + try: + m = _read_meta(obj) + if not m or m.get("type") != "stempel": continue + if m.get("stempel_scope") in target_scopes: + ids.append(m["id"]) + except Exception: pass + if not ids: return + _was = sc.sticky.get(_REGEN_BUSY, False) + sc.sticky[_REGEN_BUSY] = True + try: + for sid in ids: + try: _regenerate_element(doc, sid) + except Exception as ex: + print("[ELEMENTE] stempel-regen", sid, ":", ex) + finally: + sc.sticky[_REGEN_BUSY] = _was + + def _make_raum_hatch(outline_curve, z_uk, doc, pattern_name="Solid"): """Erzeugt einen Hatch unter der Raum-Outline am Z=z_uk + 1 mm mit gegebenem Pattern. Color = ByObject (default hell). Override-System @@ -5572,10 +5759,22 @@ def _regenerate_element(doc, element_id): _was_busy = sc.sticky.get(_REGEN_BUSY, False) sc.sticky[_REGEN_BUSY] = True try: - return _regenerate_element_body(doc, element_id, src_obj, meta, - geom, geschoss_name) + result = _regenerate_element_body(doc, element_id, src_obj, meta, + geom, geschoss_name) finally: sc.sticky[_REGEN_BUSY] = _was_busy + # Wenn ein raum_outline-Regen einen Stempel-Dirty-Marker gesetzt hat + # (oder generell raeumlich-Aenderungen), Stempel im selben Geschoss + # (+ Total) neu rechnen. Lauft AUSSERHALB des REGEN_BUSY-Blocks damit + # die Stempel selber regen koennen. + try: + dirty_gid = sc.sticky.get("_dossier_stempel_dirty") + if dirty_gid is not None: + sc.sticky["_dossier_stempel_dirty"] = None + _regenerate_stempel_for_geschoss(doc, dirty_gid) + except Exception as ex: + print("[ELEMENTE] stempel-cascade:", ex) + return result def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name): @@ -6321,6 +6520,40 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name raum_txt_modus=meta.get("raum_txt_modus")) try: doc.Objects.AddText(te, attrs) except Exception as ex: print("[ELEMENTE] Raum AddText:", ex) + # Stempel im selben Geschoss + Total-Stempel regennen, damit ihre + # SIA-Bilanz sich aktualisiert. Deferred via Sticky-Marker damit + # das nicht im aktuellen _REGEN_BUSY-Zyklus laeuft. + try: + sc.sticky["_dossier_stempel_dirty"] = ( + meta.get("geschoss") or "") + except Exception: pass + return True + elif meta["type"] == "stempel": + # Stempel re-renderet sich SELBER (kein separates Volume) — wir + # ersetzen die TextEntity in-place. Position bleibt aus der alten + # Geometrie erhalten. + if not isinstance(geom, rg.TextEntity): + print("[ELEMENTE] stempel regen: geom ist keine TextEntity") + return False + try: + pos = geom.Plane.Origin + except Exception: + pos = rg.Point3d(0, 0, 0) + scope = meta.get("stempel_scope") or "total" + txt_h = float(meta.get("stempel_txt_h", 0.20)) + 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) + if new_te is None: return False + # In-place replace + try: + doc.Objects.Replace(src_obj.Id, new_te) + except Exception as ex: + print("[ELEMENTE] stempel Replace:", ex) + return False return True else: return False @@ -6422,6 +6655,7 @@ class ElementeBridge(panel_base.BaseBridge): elif t == "CREATE_STUETZE": self._cmd_create_stuetze(p) elif t == "CREATE_TRAEGER": self._cmd_create_traeger(p) elif t == "CREATE_RAUM": self._cmd_create_raum(p) + elif t == "CREATE_STEMPEL": self._cmd_create_stempel(p) elif t == "EXPORT_RAEUME": self._cmd_export_raeume(p) elif t == "LIST_LIBRARY": self._cmd_list_library(p) elif t == "CREATE_SYMBOL": self._cmd_create_symbol(p) @@ -6678,6 +6912,23 @@ class ElementeBridge(panel_base.BaseBridge): "areaFmt": _format_area(area, rnd_eff), "umfang": perim, }) + elif meta["type"] == "stempel": + # Aggregierter Bilanz-Stempel (SIA 416). Scope = "total" oder + # "geschoss:". Liefert direkt die berechneten Werte fuer + # ein evtl. Properties-Panel. + scope = meta.get("stempel_scope") or "total" + bilanz = compute_sia_bilanz(doc, scope) + base.update({ + "kind": "stempel", + "scope": scope, + "scopeLabel": ("Total" if scope == "total" + else (bilanz.get("geschossName") or "—")), + "txtH": meta.get("stempel_txt_h", 0.20), + "font": meta.get("stempel_font", "") or "", + "bold": bool(meta.get("stempel_bold", False)), + "italic": bool(meta.get("stempel_italic", False)), + "bilanz": bilanz, + }) elif meta["type"] in ("stuetze_point", "traeger_axis"): # Tragwerk: Stuetze (Punkt) oder Traeger/Unterzug (Achse) gs = _geschoss_by_id(doc, meta["geschoss"]) @@ -8415,6 +8666,55 @@ class ElementeBridge(panel_base.BaseBridge): print("[ELEMENTE] Raum erzeugt: {} ({})".format(name, raum_id)) self._send_state() + def _cmd_create_stempel(self, p): + """Erzeugt einen SIA-Bilanz-Stempel (TextEntity) an einem User- + gepickten Punkt. Default-Scope = aktives Geschoss (oder 'total' + wenn p.scope='total' gepasst wird). Stempel re-rendert sich + automatisch wenn sich Raeume im Scope aendern.""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + scope = p.get("scope") or "" + if not scope: + # Default: aktives Geschoss, sonst total + gid = _active_geschoss_id(doc) + scope = "geschoss:{}".format(gid) if gid else "total" + try: txt_h = float(p.get("txtH") or _last("stempel_txt_h", 0.20)) + except Exception: txt_h = 0.20 + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + except Exception as ex: + print("[ELEMENTE] CREATE_STEMPEL imports:", ex); return + gp = ric.GetPoint() + gp.SetCommandPrompt("Stempel platzieren ({})".format(scope)) + res = gp.Get() + if res != GetResult.Point: return + pos = gp.Point() + # Element-ID + Layer + stempel_id = "stempel_" + uuid.uuid4().hex[:10] + # Layer: nutze Raeume-Sublayer des AKTIVEN Geschosses (oder ersten), + # damit Stempel beim CPlane-Clipping auf derselben Ebene sitzt wie + # die Raeume + active_g_name = _active_geschoss_name(doc) or "EG" + layer = _ensure_layer(doc, _layer_path_raum(doc, active_g_name)) + # TextEntity bauen + te = _make_stempel_text(pos, scope, doc, text_height=txt_h, z=pos.Z) + if te is None: + print("[ELEMENTE] CREATE_STEMPEL: Text-Build failed"); return + attrs = Rhino.DocObjects.ObjectAttributes() + attrs.LayerIndex = layer + _attach_meta(attrs, stempel_id, "stempel", + _active_geschoss_id(doc) or "", + 0.0, "", "", "mid", + stempel_scope=scope, stempel_txt_h=txt_h) + new_id = doc.Objects.AddText(te, attrs) + if new_id == System.Guid.Empty: + print("[ELEMENTE] Stempel AddText fehlgeschlagen"); return + _save_last(stempel_txt_h=txt_h) + doc.Views.Redraw() + print("[ELEMENTE] Stempel erzeugt: {} (scope={})".format(stempel_id, scope)) + self._send_state() + def _cmd_export_raeume(self, p): """Schreibt CSV mit allen Raeumen: Nummer, Name, Geschoss, Funktion, SIA, Flaeche, Umfang. Datei via SaveFileDialog.""" @@ -10402,6 +10702,29 @@ class ElementeBridge(panel_base.BaseBridge): doc.Views.Redraw() self._send_state() return + # Stempel: scope / txtH / font / bold / italic + if old_meta["type"] == "stempel": + scope = p.get("scope", old_meta.get("stempel_scope", "total")) + if not (scope == "total" or scope.startswith("geschoss:")): + scope = "total" + try: st_th = float(p.get("txtH", old_meta.get("stempel_txt_h", 0.20))) + except Exception: st_th = 0.20 + 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))) + 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) + axis_obj.Attributes = attrs + axis_obj.CommitChanges() + _save_last(stempel_txt_h=st_th) + _regenerate_element(doc, wall_id) + doc.Views.Redraw() + self._send_state() + return # Treppe: Breite/Anzahl Stufen/Referenz/Zielgeschoss if old_meta["type"] == "treppe_axis": try: tb = float(p.get("breite", old_meta.get("treppe_breite", 1.0))) diff --git a/rhino/elemente_uebersicht.py b/rhino/elemente_uebersicht.py index b108681..82636f6 100644 --- a/rhino/elemente_uebersicht.py +++ b/rhino/elemente_uebersicht.py @@ -31,6 +31,7 @@ _KIND_MAP = { "stuetze_point": "stuetze", "traeger_axis": "traeger", "raum_outline": "raum", + "stempel": "stempel", "decke_aussparung_outline": "aussparung", "oeffnung_point": "oeffnung", # wird zu fenster/tuer aufgeloest } @@ -131,18 +132,21 @@ def _build_overview(doc): 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"): + 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 + Total ableiten + # NF/NGF/Total ableiten for b in sia_bilanz.values(): b["nf"] = b["hnf"] + b["nnf"] - b["total"] = b["hnf"] + b["nnf"] + b["vf"] + b["ff"] + b["ohne"] + 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} diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx index 1679be5..37fe844 100644 --- a/src/ElementeApp.jsx +++ b/src/ElementeApp.jsx @@ -7,7 +7,7 @@ import { onMessage, notifyReady, createWall, createDecke, createDach, createFenster, createTuer, createAussparung, createTreppe, - createStuetze, createTraeger, createRaum, + createStuetze, createTraeger, createRaum, createStempel, openSwisstopo, openSwisstopoDialog, openOsmDialog, updateElement, deleteElement, openElementeUebersicht, openElementeProperties, saveOeffStyle, deleteOeffStyle, @@ -178,6 +178,7 @@ const KIND_META = { stuetze: { icon: 'square_foot', label: 'Stütze', color: '#5fa896' }, traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#7fc8a8' }, raum: { icon: 'crop_free', label: 'Raum', color: '#a0a8b0' }, + stempel: { icon: 'receipt_long', label: 'Stempel', color: '#5fa896' }, aussparung: { icon: 'rectangle', label: 'Aussparung', color: '#9090a0' }, } @@ -193,6 +194,8 @@ const RAUM_SIA_KINDS = [ { code: 'nnf', label: 'NNF', color: '#e8c498', hint: 'Nebennutzfläche' }, { code: 'vf', label: 'VF', color: '#e8d878', hint: 'Verkehrsfläche' }, { code: 'ff', label: 'FF', color: '#a8c8e0', hint: 'Funktionsfläche' }, + { code: 'gf', label: 'GF', color: '#d0d0d0', hint: 'Geschossfläche (gross — umfasst das ganze Geschoss)' }, + { code: 'agf', label: 'AGF', color: '#c0d8c0', hint: 'Aussengeschossfläche (Balkone, Terrassen)' }, ] const PROFIL_META = { @@ -479,6 +482,11 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) { 'Outline zeichnen · Stempel zeigt Name + Fläche'} disabled={dis} onClick={() => createRaum({})} /> + createStempel({})} /> @@ -529,6 +537,9 @@ export function PropertiesView({ selected, geschosse, materials, hatchPatterns, hatchPatterns={hatchPatterns} fonts={fonts || []} raumStempelStile={raumStempelStile || []} onUpdate={upd} onDelete={del('Raum')} /> + if (selected.kind === 'stempel') + return if (selected.kind === 'aussparung') return @@ -1139,6 +1150,97 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo ) } +function StempelProperties({ stempel, geschosse, onUpdate, onDelete }) { + const scope = stempel.scope || 'total' + const bilanz = stempel.bilanz || {} + 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, + 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, + 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) + return ( +
+
+ + + Stempel · {stempel.scopeLabel || '—'} + + +
+ +
+ Scope + +
+ +
+
+ Bilanz · {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. +
+ ) : rows.map(r => ( +
+ {r.label} + {r.val.toFixed(1)} m² +
+ ))} +
+ +
+ + Typografie (Font/Stil/Höhe): Stempel im Viewport selektieren → + Oberleiste. +
+
+ ) +} + + function AussparungProperties({ aussp, onDelete }) { return (