Files
DOSSIER/rhino/elemente_uebersicht.py
karim 61923e1b2b SIA-Bilanz CSV-Export aus Elemente-Uebersicht
Download-Button (file_download Icon) neben den Expand/Collapse-Buttons
in der Toolbar. Klick → SaveFileDialog (Default 'sia_bilanz.csv') →
schreibt Excel-kompatible CSV.

Backend (elemente_uebersicht._export_bilanz):
- Wide-Format: Spalten = Kategorie + ein Geschoss + Total
- Zeilen: HNF, NNF, NF, VF, FF, NGF, GF, AGF, Räume (count), Personen
- Werte via _elm.compute_sia_bilanz fuer jeden Scope (gleiche Logik
  wie Bilanz-Stempel + Uebersicht — single source of truth)
- Format: Semikolon-Separator + UTF-8 BOM + Komma als Dezimaltrenner
  (Excel CH/DE-kompatibel, oeffnet ohne Umweg)
- Flaechen mit 2 Nachkommastellen, Raume/Personen als int
2026-05-27 01:13:08 +02:00

304 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#! python 3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
elemente_uebersicht.py
BIM-artiger Project Browser: alle Smart-Elemente in einem Tree
gruppiert nach Geschoss → Kind → Element. Eigene Satellite-Window
(Eto.Form + WebView), liest seine Daten direkt aus dem ActiveDoc
via elemente._read_meta. Klick auf eine Zeile selektiert das Objekt
in Rhino.
"""
import os
import sys
import Rhino
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
import elemente as _elm
_KIND_MAP = {
"wand_axis": "wand",
"decke_outline": "decke",
"dach_outline": "dach",
"treppe_axis": "treppe",
"stuetze_point": "stuetze",
"traeger_axis": "traeger",
"raum_outline": "raum",
"stempel": "stempel",
"decke_aussparung_outline": "aussparung",
"oeffnung_point": "oeffnung", # wird zu fenster/tuer aufgeloest
}
def _safe_float(v, default=None):
try: return float(v)
except Exception: return default
def _build_overview(doc):
"""Sammelt alle Smart-Element-Sources, gruppiert nach Geschoss +
Kind. Returns dict mit 'geschosse' (geordnete Liste) + 'items'
(Flat-Liste pro Geschoss/Kind). Frontend baut den Tree."""
if doc is None:
return {"geschosse": [], "items": []}
geschosse = _elm._load_geschosse(doc) or []
items = []
seen = set()
for obj in doc.Objects:
meta = _elm._read_meta(obj)
if meta is None: continue
t = meta.get("type")
if t not in _elm.SOURCE_TYPES: continue
if meta["id"] in seen: continue
seen.add(meta["id"])
kind = _KIND_MAP.get(t, t)
if t == "oeffnung_point":
kind = meta.get("oeff_typ", "fenster")
g = _elm._geschoss_by_id(doc, meta.get("geschoss"))
g_id = (g.get("id") if g else "") or "__keingeschoss__"
g_name = g.get("name") if g else "(kein Geschoss)"
# Kompakte Property-Zusammenfassung pro Element-Typ
info = ""
try:
if kind == "wand":
info = "d {:.2f} m".format(meta.get("dicke", 0) or 0)
elif kind == "decke":
info = "d {:.2f} m".format(meta.get("dicke", 0) or 0)
elif kind == "dach":
info = "d {:.2f} m · {:.0f}°".format(
meta.get("dicke", 0) or 0, meta.get("neigung", 0) or 0)
elif kind in ("fenster", "tuer"):
info = "{:.2f}×{:.2f} m".format(
meta.get("oeff_breite", 0) or 0,
meta.get("oeff_hoehe", 0) or 0)
elif kind == "treppe":
info = "{} St".format(meta.get("treppe_n_stufen", "?"))
elif kind in ("stuetze", "traeger"):
profil = meta.get("trag_profil", "?")
info = "{}".format(profil)
elif kind == "raum":
info = meta.get("raum_name", "") or "Raum"
elif kind == "aussparung":
info = "Aussparung"
except Exception: pass
items.append({
"id": meta["id"],
"objectId": str(obj.Id),
"kind": kind,
"geschossId": g_id,
"geschossName": g_name,
"name": meta.get("raum_name") or "",
"info": info,
"selected": obj.IsSelected(False) > 0,
})
# Geschoss-Liste (geordnet wie in doc.Strings)
out_geschosse = []
for g in geschosse:
if not isinstance(g, dict): continue
out_geschosse.append({
"id": g.get("id") or "",
"name": g.get("name") or "?",
"okff": _safe_float(g.get("okff"), 0.0),
})
# "(kein Geschoss)" anhaengen wenn es Elemente ohne Geschoss gibt
if any(it["geschossId"] == "__keingeschoss__" for it in items):
out_geschosse.append({
"id": "__keingeschoss__", "name": "(kein Geschoss)", "okff": None,
})
# SIA-416 Bilanz pro Geschoss: aggregiert alle raum_outline-Flaechen
# nach raum_sia-Klassifikation. Räume ohne SIA-Tag landen in "ohne".
# NF = HNF + NNF (Nutzflaeche). Wird im Frontend als Tabelle gerendert.
sia_bilanz = {} # {geschossId: {hnf, nnf, vf, ff, ohne, nf, total, count}}
for obj in doc.Objects:
meta = _elm._read_meta(obj)
if meta is None: continue
if meta.get("type") != "raum_outline": continue
try:
area, _, _ = _elm._raum_amp(obj.Geometry)
except Exception: continue
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", "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/NGF/Total ableiten
for b in sia_bilanz.values():
b["nf"] = b["hnf"] + b["nnf"]
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}
class ElementeUebersichtBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "elemente_uebersicht")
def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc
self.send("STATE", _build_overview(doc))
def _on_ready(self):
self._send_state()
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
doc = Rhino.RhinoDoc.ActiveDoc
if t == "READY" or t == "REQUEST_STATE":
self._on_ready()
elif t == "SELECT_ELEMENT":
obj_id_str = p.get("objectId") or ""
try:
import System
guid = System.Guid(obj_id_str)
obj = doc.Objects.FindId(guid)
if obj is not None:
doc.Objects.UnselectAll()
obj.Select(True)
try: doc.Views.Redraw()
except Exception: pass
except Exception as ex:
print("[UEBERSICHT] select:", ex)
self._send_state()
elif t == "ZOOM_TO_ELEMENT":
obj_id_str = p.get("objectId") or ""
try:
import System
guid = System.Guid(obj_id_str)
obj = doc.Objects.FindId(guid)
if obj is not None:
doc.Objects.UnselectAll()
obj.Select(True)
try:
vp = doc.Views.ActiveView.ActiveViewport
bb = obj.Geometry.GetBoundingBox(True)
if bb.IsValid:
bb.Inflate(bb.Diagonal.Length * 0.5,
bb.Diagonal.Length * 0.5,
bb.Diagonal.Length * 0.5)
vp.ZoomBoundingBox(bb)
doc.Views.Redraw()
except Exception as ex:
print("[UEBERSICHT] zoom:", ex)
except Exception as ex:
print("[UEBERSICHT] zoom find:", ex)
elif t == "EXPORT_BILANZ":
self._export_bilanz()
def _export_bilanz(self):
"""Exportiert SIA-416 Bilanz als CSV (Excel-kompatibel: Semikolon-
Separator + UTF-8 BOM + Komma als Dezimaltrenner). Wide-Format:
eine Spalte pro Geschoss + Total-Spalte, Zeilen pro Kategorie.
"""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
# Geschoss-Liste (geordnet) + Total am Ende
geschosse = _elm._load_geschosse(doc) or []
gs_list = [g for g in geschosse
if isinstance(g, dict) and g.get("isGeschoss")]
# Bilanz pro Geschoss + Total via compute_sia_bilanz
per_gid = {} # gid → bilanz dict
for g in gs_list:
per_gid[g["id"]] = _elm.compute_sia_bilanz(
doc, "geschoss:" + g["id"])
total = _elm.compute_sia_bilanz(doc, "total")
# SaveFileDialog
try:
from Rhino.UI import SaveFileDialog
sfd = SaveFileDialog()
sfd.DefaultExt = "csv"
sfd.Filter = "CSV (*.csv)|*.csv"
sfd.FileName = "sia_bilanz.csv"
ok = False
try: ok = sfd.ShowSaveDialog()
except Exception:
try: ok = sfd.ShowDialog()
except Exception: ok = False
if not ok:
print("[UEBERSICHT] Bilanz-Export abgebrochen"); return
path = sfd.FileName
except Exception as ex:
print("[UEBERSICHT] SaveFileDialog:", ex); return
# Zeilen-Definition: (Label, Bilanz-Key, ist_personen?)
rows = [
("HNF (m²)", "hnf", False),
("NNF (m²)", "nnf", False),
("NF (m²)", "nf", False),
("VF (m²)", "vf", False),
("FF (m²)", "ff", False),
("NGF (m²)", "ngf", False),
("GF (m²)", "gf", False),
("AGF (m²)", "agf", False),
("Räume", "count", True),
("Personen", "personen", True),
]
def _fmt(val, is_count):
if val is None: return ""
if is_count: return str(int(val))
return "{:.2f}".format(float(val)).replace(".", ",")
def _esc(s):
s = str(s)
if ";" in s or '"' in s or "\n" in s:
return '"' + s.replace('"', '""') + '"'
return s
try:
import io
with io.open(path, "w", encoding="utf-8-sig", newline="") as f:
# Header — Kategorie + Geschoss-Namen + Total
header = ["Kategorie"]
for g in gs_list: header.append(_esc(g.get("name") or "?"))
header.append("Total")
f.write(";".join(header) + "\n")
for label, key, is_count in rows:
line = [_esc(label)]
for g in gs_list:
b = per_gid.get(g["id"], {})
line.append(_fmt(b.get(key, 0), is_count))
line.append(_fmt(total.get(key, 0), is_count))
f.write(";".join(line) + "\n")
print("[UEBERSICHT] SIA-Bilanz exportiert: {} ({} Geschosse + Total)".format(
path, len(gs_list)))
except Exception as ex:
print("[UEBERSICHT] CSV schreiben:", ex)
def open_as_window():
"""Oeffnet die Element-Uebersicht als Satellite-Window."""
b = ElementeUebersichtBridge()
sc.sticky["elemente_uebersicht_bridge"] = b
panel_base.open_satellite_window(
"elemente_uebersicht",
title="Elemente — Übersicht",
size=(540, 720),
bridge=b)