Stempel-Element: SIA-Bilanz als platzierbares Viewport-Objekt

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:<gid>"
  - 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.
This commit is contained in:
2026-05-27 00:10:02 +02:00
parent 7fbda8c289
commit 2386366566
5 changed files with 440 additions and 9 deletions
+327 -4
View File
@@ -408,18 +408,23 @@ def _resolve_raum_rundung(meta, doc=None):
_RAUM_ALIGN = ("links", "mid", "rechts") _RAUM_ALIGN = ("links", "mid", "rechts")
_RAUM_SIA_KINDS = ("", "hnf", "nnf", "vf", "ff") _RAUM_SIA_KINDS = ("", "hnf", "nnf", "vf", "ff", "gf", "agf")
_RAUM_FUNKTIONEN = ( _RAUM_FUNKTIONEN = (
"wohnen", "schlafen", "bad", "kueche", "essen", "flur", "diele", "wohnen", "schlafen", "bad", "kueche", "essen", "flur", "diele",
"buero", "atelier", "lager", "technik", "balkon", "terrasse", "buero", "atelier", "lager", "technik", "balkon", "terrasse",
"sonstiges", "sonstiges",
) )
# SIA-416 Farbpalette nach CH-Buero-Konvention (helle, kraeftige Pastelltoene). # 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 = { _SIA_COLORS_HEX = {
"hnf": "#e8a8a8", # Hauptnutzflaeche — Rot "hnf": "#e8a8a8", # Hauptnutzflaeche — Rot
"nnf": "#e8c498", # Nebennutzflaeche — Orange "nnf": "#e8c498", # Nebennutzflaeche — Orange
"vf": "#e8d878", # Verkehrsflaeche — Gelb "vf": "#e8d878", # Verkehrsflaeche — Gelb
"ff": "#a8c8e0", # Funktionsflaeche — Hellblau "ff": "#a8c8e0", # Funktionsflaeche — Hellblau
"gf": "#d0d0d0", # Geschossflaeche (gross) — Grau
"agf": "#c0d8c0", # Aussengeschossflaeche — Hellgruen
} }
_SIA_LABELS = { _SIA_LABELS = {
"": "", "": "",
@@ -427,6 +432,8 @@ _SIA_LABELS = {
"nnf": "NNF", "nnf": "NNF",
"vf": "VF", "vf": "VF",
"ff": "FF", "ff": "FF",
"gf": "GF",
"agf": "AGF",
} }
# Cross-Doc Preset-Name fuer Override-Engine. Steuert auch das siaFillMode- # 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_show_sia=None,
raum_stamp_dx=None, raum_stamp_dy=None, raum_stamp_dx=None, raum_stamp_dy=None,
raum_layout=None, raum_txt_modus=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_layered=None, wand_layers=None, wand_layer_idx=None,
wand_chain_members=None, wand_chain_members=None,
aussp_parent=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) print("[ELEMENTE] raum_layout set:", ex)
if raum_txt_modus is not None and raum_txt_modus in ("fix", "masstab"): if raum_txt_modus is not None and raum_txt_modus in ("fix", "masstab"):
obj_attrs.SetUserString(_KEY_RAUM_TXT_MODUS, raum_txt_modus) 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 # 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,
@@ -3080,6 +3104,13 @@ def _read_meta(obj):
if r_txt_modus not in ("fix", "masstab"): r_txt_modus = "fix" if r_txt_modus not in ("fix", "masstab"): r_txt_modus = "fix"
# Aktiver Stempel-Stil (id des zuletzt applizierten Stils) # Aktiver Stempel-Stil (id des zuletzt applizierten Stils)
r_stil_id = a.GetUserString(_KEY_RAUM_STIL_ID) or "" 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 # 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 = []
@@ -3202,6 +3233,11 @@ def _read_meta(obj):
"raum_layout": r_layout, "raum_layout": r_layout,
"raum_txt_modus": r_txt_modus, "raum_txt_modus": r_txt_modus,
"raum_stil_id": r_stil_id, "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_layered": w_layered,
"wand_layers": w_layers, "wand_layers": w_layers,
"wand_layer_idx": w_layer_idx, "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", SOURCE_TYPES = ("wand_axis", "decke_outline", "dach_outline",
"oeffnung_point", "treppe_axis", "oeffnung_point", "treppe_axis",
"stuetze_point", "traeger_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", VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume",
"oeffnung_volume", "oeffnung_swing", "oeffnung_sturz", "oeffnung_volume", "oeffnung_swing", "oeffnung_sturz",
"treppe_volume", "stuetze_volume", "traeger_volume", "treppe_volume", "stuetze_volume", "traeger_volume",
"raum_stamp", "raum_fill") "raum_stamp", "raum_fill")
# Stempel-Scope-Werte:
# "total" → alle Raeume im Doc
# "geschoss:<id>" → 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 # 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.
@@ -4938,6 +4987,144 @@ def _make_raum_stamp_text(centroid, name, nummer, funktion, area, rundung,
return None return None
def compute_sia_bilanz(doc, scope="total"):
"""Aggregiert raum_outline-Flaechen nach raum_sia-Klassifikation.
scope = "total" alle Raeume
scope = "geschoss:<id>" 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 "{} {}".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:<id>"). 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"): 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 """Erzeugt einen Hatch unter der Raum-Outline am Z=z_uk + 1 mm mit
gegebenem Pattern. Color = ByObject (default hell). Override-System 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) _was_busy = sc.sticky.get(_REGEN_BUSY, False)
sc.sticky[_REGEN_BUSY] = True sc.sticky[_REGEN_BUSY] = True
try: try:
return _regenerate_element_body(doc, element_id, src_obj, meta, result = _regenerate_element_body(doc, element_id, src_obj, meta,
geom, geschoss_name) geom, geschoss_name)
finally: finally:
sc.sticky[_REGEN_BUSY] = _was_busy 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): 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")) raum_txt_modus=meta.get("raum_txt_modus"))
try: doc.Objects.AddText(te, attrs) try: doc.Objects.AddText(te, attrs)
except Exception as ex: print("[ELEMENTE] Raum AddText:", ex) 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 return True
else: else:
return False return False
@@ -6422,6 +6655,7 @@ class ElementeBridge(panel_base.BaseBridge):
elif t == "CREATE_STUETZE": self._cmd_create_stuetze(p) elif t == "CREATE_STUETZE": self._cmd_create_stuetze(p)
elif t == "CREATE_TRAEGER": self._cmd_create_traeger(p) elif t == "CREATE_TRAEGER": self._cmd_create_traeger(p)
elif t == "CREATE_RAUM": self._cmd_create_raum(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 == "EXPORT_RAEUME": self._cmd_export_raeume(p)
elif t == "LIST_LIBRARY": self._cmd_list_library(p) elif t == "LIST_LIBRARY": self._cmd_list_library(p)
elif t == "CREATE_SYMBOL": self._cmd_create_symbol(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), "areaFmt": _format_area(area, rnd_eff),
"umfang": perim, "umfang": perim,
}) })
elif meta["type"] == "stempel":
# Aggregierter Bilanz-Stempel (SIA 416). Scope = "total" oder
# "geschoss:<gid>". 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"): elif meta["type"] in ("stuetze_point", "traeger_axis"):
# Tragwerk: Stuetze (Punkt) oder Traeger/Unterzug (Achse) # Tragwerk: Stuetze (Punkt) oder Traeger/Unterzug (Achse)
gs = _geschoss_by_id(doc, meta["geschoss"]) gs = _geschoss_by_id(doc, meta["geschoss"])
@@ -8415,6 +8666,55 @@ class ElementeBridge(panel_base.BaseBridge):
print("[ELEMENTE] Raum erzeugt: {} ({})".format(name, raum_id)) print("[ELEMENTE] Raum erzeugt: {} ({})".format(name, raum_id))
self._send_state() 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): def _cmd_export_raeume(self, p):
"""Schreibt CSV mit allen Raeumen: Nummer, Name, Geschoss, """Schreibt CSV mit allen Raeumen: Nummer, Name, Geschoss,
Funktion, SIA, Flaeche, Umfang. Datei via SaveFileDialog.""" Funktion, SIA, Flaeche, Umfang. Datei via SaveFileDialog."""
@@ -10402,6 +10702,29 @@ 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
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 # Treppe: Breite/Anzahl Stufen/Referenz/Zielgeschoss
if old_meta["type"] == "treppe_axis": if old_meta["type"] == "treppe_axis":
try: tb = float(p.get("breite", old_meta.get("treppe_breite", 1.0))) try: tb = float(p.get("breite", old_meta.get("treppe_breite", 1.0)))
+7 -3
View File
@@ -31,6 +31,7 @@ _KIND_MAP = {
"stuetze_point": "stuetze", "stuetze_point": "stuetze",
"traeger_axis": "traeger", "traeger_axis": "traeger",
"raum_outline": "raum", "raum_outline": "raum",
"stempel": "stempel",
"decke_aussparung_outline": "aussparung", "decke_aussparung_outline": "aussparung",
"oeffnung_point": "oeffnung", # wird zu fenster/tuer aufgeloest "oeffnung_point": "oeffnung", # wird zu fenster/tuer aufgeloest
} }
@@ -131,18 +132,21 @@ def _build_overview(doc):
if not area or area <= 0: continue if not area or area <= 0: continue
g_id = meta.get("geschoss") or "__keingeschoss__" g_id = meta.get("geschoss") or "__keingeschoss__"
sia = (meta.get("raum_sia") or "").lower() 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" sia = "ohne"
b = sia_bilanz.setdefault(g_id, { b = sia_bilanz.setdefault(g_id, {
"hnf": 0.0, "nnf": 0.0, "vf": 0.0, "ff": 0.0, "hnf": 0.0, "nnf": 0.0, "vf": 0.0, "ff": 0.0,
"gf": 0.0, "agf": 0.0,
"ohne": 0.0, "count": 0, "ohne": 0.0, "count": 0,
}) })
b[sia] += float(area) b[sia] += float(area)
b["count"] += 1 b["count"] += 1
# NF + Total ableiten # NF/NGF/Total ableiten
for b in sia_bilanz.values(): for b in sia_bilanz.values():
b["nf"] = b["hnf"] + b["nnf"] 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, return {"geschosse": out_geschosse, "items": items,
"siaBilanz": sia_bilanz} "siaBilanz": sia_bilanz}
+103 -1
View File
@@ -7,7 +7,7 @@ import {
onMessage, notifyReady, onMessage, notifyReady,
createWall, createDecke, createDach, createWall, createDecke, createDach,
createFenster, createTuer, createAussparung, createTreppe, createFenster, createTuer, createAussparung, createTreppe,
createStuetze, createTraeger, createRaum, createStuetze, createTraeger, createRaum, createStempel,
openSwisstopo, openSwisstopoDialog, openOsmDialog, openSwisstopo, openSwisstopoDialog, openOsmDialog,
updateElement, deleteElement, openElementeUebersicht, openElementeProperties, updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
saveOeffStyle, deleteOeffStyle, saveOeffStyle, deleteOeffStyle,
@@ -178,6 +178,7 @@ const KIND_META = {
stuetze: { icon: 'square_foot', label: 'Stütze', color: '#5fa896' }, stuetze: { icon: 'square_foot', label: 'Stütze', color: '#5fa896' },
traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#7fc8a8' }, traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#7fc8a8' },
raum: { icon: 'crop_free', label: 'Raum', color: '#a0a8b0' }, raum: { icon: 'crop_free', label: 'Raum', color: '#a0a8b0' },
stempel: { icon: 'receipt_long', label: 'Stempel', color: '#5fa896' },
aussparung: { icon: 'rectangle', label: 'Aussparung', color: '#9090a0' }, 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: 'nnf', label: 'NNF', color: '#e8c498', hint: 'Nebennutzfläche' },
{ code: 'vf', label: 'VF', color: '#e8d878', hint: 'Verkehrsfläche' }, { code: 'vf', label: 'VF', color: '#e8d878', hint: 'Verkehrsfläche' },
{ code: 'ff', label: 'FF', color: '#a8c8e0', hint: 'Funktionsflä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 = { const PROFIL_META = {
@@ -479,6 +482,11 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
'Outline zeichnen · Stempel zeigt Name + Fläche'} 'Outline zeichnen · Stempel zeigt Name + Fläche'}
disabled={dis} disabled={dis}
onClick={() => createRaum({})} /> onClick={() => createRaum({})} />
<PillButton icon="receipt_long" label="Stempel"
hint={dis ? baseHint('Stempel') :
'SIA-Bilanz-Stempel platzieren · Default = aktives Geschoss · Properties: Total/Geschoss umstellen'}
disabled={dis}
onClick={() => createStempel({})} />
</PillGroup> </PillGroup>
<PillGroup label="Library"> <PillGroup label="Library">
@@ -529,6 +537,9 @@ export function PropertiesView({ selected, geschosse, materials, hatchPatterns,
hatchPatterns={hatchPatterns} fonts={fonts || []} hatchPatterns={hatchPatterns} fonts={fonts || []}
raumStempelStile={raumStempelStile || []} raumStempelStile={raumStempelStile || []}
onUpdate={upd} onDelete={del('Raum')} /> onUpdate={upd} onDelete={del('Raum')} />
if (selected.kind === 'stempel')
return <StempelProperties stempel={selected} geschosse={geschosse}
onUpdate={upd} onDelete={del('Stempel')} />
if (selected.kind === 'aussparung') if (selected.kind === 'aussparung')
return <AussparungProperties aussp={selected} onDelete={del('Aussparung')} /> return <AussparungProperties aussp={selected} onDelete={del('Aussparung')} />
@@ -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 (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Icon name="receipt_long" size={13}
style={{ color: 'var(--accent)', marginRight: 6 }} />
<span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}>
Stempel · {stempel.scopeLabel || '—'}
</span>
<button className="btn-icon-sm btn-icon-danger" onClick={onDelete}
title="Löschen">
<Icon name="delete" size={12} />
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Scope</span>
<select value={scope}
onChange={(e) => onUpdate({ scope: e.target.value })}
title="Welche Räume werden im Stempel aggregiert"
style={{ flex: 1, fontSize: 11 }}>
<option value="total">Total · alle Geschosse</option>
{geschosse.filter(g => g.id !== '__keingeschoss__').map(g => (
<option key={g.id} value={`geschoss:${g.id}`}>
Geschoss · {g.name}
</option>
))}
</select>
</div>
<div style={{
paddingTop: 6, borderTop: '1px dashed var(--border)',
fontSize: 10, fontFamily: 'DM Mono, monospace',
color: 'var(--text-muted)',
}}>
<div style={{ fontSize: 9, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase',
marginBottom: 4 }}>
Bilanz · {bilanz.count || 0} Räume klassifiziert
</div>
{rows.length === 0 ? (
<div style={{ fontSize: 10, color: 'var(--text-muted)',
fontStyle: 'italic', padding: '4px 0' }}>
Keine klassifizierten Räume im Scope.
Räume mit SIA-Tag (HNF/NNF/VF/FF/GF/AGF) erscheinen hier.
</div>
) : rows.map(r => (
<div key={r.key} title={r.hint || ''}
style={{ display: 'flex', justifyContent: 'space-between',
padding: '2px 0',
color: r.accent ? 'var(--accent)' : 'inherit',
fontWeight: r.accent ? 600 : 400 }}>
<span>{r.label}</span>
<span>{r.val.toFixed(1)} </span>
</div>
))}
</div>
<div style={{
fontSize: 9, color: 'var(--text-muted)',
paddingTop: 4, borderTop: '1px dashed var(--border)',
display: 'flex', alignItems: 'center', gap: 4,
}}>
<Icon name="info" size={11} style={{ color: 'var(--text-muted)' }} />
Typografie (Font/Stil/Höhe): Stempel im Viewport selektieren
Oberleiste.
</div>
</div>
)
}
function AussparungProperties({ aussp, onDelete }) { function AussparungProperties({ aussp, onDelete }) {
return ( return (
<div style={{ <div style={{
+2 -1
View File
@@ -10,7 +10,7 @@ import { onMessage, notifyReady, send } from './lib/rhinoBridge'
const KIND_ORDER = [ const KIND_ORDER = [
'wand', 'decke', 'dach', 'fenster', 'tuer', 'aussparung', 'wand', 'decke', 'dach', 'fenster', 'tuer', 'aussparung',
'treppe', 'stuetze', 'traeger', 'raum', 'treppe', 'stuetze', 'traeger', 'raum', 'stempel',
] ]
const KIND_META = { const KIND_META = {
@@ -24,6 +24,7 @@ const KIND_META = {
stuetze: { icon: 'square_foot', label: 'Stützen', color: '#c87050' }, stuetze: { icon: 'square_foot', label: 'Stützen', color: '#c87050' },
traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#a87858' }, traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#a87858' },
raum: { icon: 'crop_free', label: 'Räume', color: '#5fa896' }, raum: { icon: 'crop_free', label: 'Räume', color: '#5fa896' },
stempel: { icon: 'receipt_long', label: 'Stempel', color: '#5fa896' },
} }
+1
View File
@@ -379,6 +379,7 @@ export function createTreppe(p) { send('CREATE_TREPPE', p || {}) }
export function createStuetze(p) { send('CREATE_STUETZE', p || {}) } export function createStuetze(p) { send('CREATE_STUETZE', p || {}) }
export function createTraeger(p) { send('CREATE_TRAEGER', p || {}) } export function createTraeger(p) { send('CREATE_TRAEGER', p || {}) }
export function createRaum(p) { send('CREATE_RAUM', p || {}) } export function createRaum(p) { send('CREATE_RAUM', p || {}) }
export function createStempel(p) { send('CREATE_STEMPEL', p || {}) }
export function exportRaeume() { send('EXPORT_RAEUME', {}) } export function exportRaeume() { send('EXPORT_RAEUME', {}) }
// Library-Symbol/Object — Picker im Elemente-Panel // Library-Symbol/Object — Picker im Elemente-Panel
export function listLibrary() { send('LIST_LIBRARY', {}) } export function listLibrary() { send('LIST_LIBRARY', {}) }