diff --git a/rhino/elemente.py b/rhino/elemente.py
index 3cdf70f..e4fc5e0 100644
--- a/rhino/elemente.py
+++ b/rhino/elemente.py
@@ -4684,6 +4684,20 @@ class ElementeBridge(panel_base.BaseBridge):
elif t == "DELETE_WALL": self._delete_wall(p.get("id"))
elif t == "DELETE_ELEMENT": self._delete_wall(p.get("id"))
elif t == "REGENERATE_ALL": self._regenerate_all()
+ elif t == "OPEN_ELEMENTE_UEBERSICHT":
+ try:
+ import elemente_uebersicht
+ elemente_uebersicht.open_as_window()
+ except Exception as ex:
+ print("[ELEMENTE] open uebersicht:", ex)
+ elif t == "OPEN_ELEMENTE_PROPERTIES":
+ try:
+ import elemente_properties
+ elemente_properties.open_as_window()
+ # Direkt mal pushen damit das Fenster sofort Daten hat
+ self._send_state()
+ except Exception as ex:
+ print("[ELEMENTE] open properties:", ex)
def _notify_active_geschoss(self):
"""Schlanker Partial-Push: nur activeGeschoss + activeGeschossName.
@@ -4884,7 +4898,7 @@ class ElementeBridge(panel_base.BaseBridge):
})
elements.append(base)
sel_id = next((e["id"] for e in elements if e["selected"]), None)
- self.send("STATE", {
+ payload = {
"elements": elements,
"geschosse": [{"id": g.get("id"), "name": g.get("name")}
for g in geschosse if isinstance(g, dict)],
@@ -4897,7 +4911,14 @@ class ElementeBridge(panel_base.BaseBridge):
{"name": n, "color": m["color"],
"hatch": m.get("hatch", ""), "scale": m.get("scale", 1.0)}
for n, m in _MATERIAL_LIBRARY.items()],
- })
+ }
+ self.send("STATE", payload)
+ # An Properties-Satellite-Window forwarden falls offen
+ try:
+ pb = sc.sticky.get("elemente_properties_bridge")
+ if pb is not None and pb is not self:
+ pb.send("STATE", payload)
+ except Exception: pass
# --- Wand-Befehle -------------------------------------------------------
diff --git a/rhino/elemente_properties.py b/rhino/elemente_properties.py
new file mode 100644
index 0000000..1d67d5b
--- /dev/null
+++ b/rhino/elemente_properties.py
@@ -0,0 +1,66 @@
+#! python 3
+# -*- coding: utf-8 -*-
+"""
+elemente_properties.py
+Properties-Satellite-Window. Zeigt die Property-Forms (WallProperties,
+RaumProperties, etc.) in einem eigenen groesseren Fenster — fuer Power-
+User die mehr Platz beim Editieren wollen ohne dass das Elemente-Panel
+ueberfrachtet wird. Daten kommen 1:1 vom ElementeBridge (sticky).
+"""
+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
+
+
+class ElementePropertiesBridge(panel_base.BaseBridge):
+ def __init__(self):
+ panel_base.BaseBridge.__init__(self, "elemente_properties")
+
+ def _send_state(self):
+ # Holt sich den aktuellen State vom Haupt-ElementeBridge — der hat
+ # die Element-Enumeration + Selection-Erkennung schon implementiert.
+ elemente_bridge = sc.sticky.get("elemente_bridge")
+ if elemente_bridge is None:
+ self.send("STATE", {"elements": [], "geschosse": [], "selection": None})
+ return
+ try:
+ elemente_bridge._send_state() # broadcast — auch wir bekommen das via sticky-Forward
+ except Exception as ex:
+ print("[ELEMENTE-PROPS] _send_state fail:", ex)
+
+ 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 = {}
+
+ if t == "READY" or t == "REQUEST_STATE":
+ self._on_ready()
+ elif t == "UPDATE_ELEMENT" or t == "DELETE_ELEMENT":
+ # Forward to main ElementeBridge — same handler
+ eb = sc.sticky.get("elemente_bridge")
+ if eb is not None:
+ try: eb.handle(data)
+ except Exception as ex:
+ print("[ELEMENTE-PROPS] forward:", ex)
+
+
+def open_as_window():
+ """Oeffnet die Properties-View als Satellite-Window."""
+ b = ElementePropertiesBridge()
+ sc.sticky["elemente_properties_bridge"] = b
+ panel_base.open_satellite_window(
+ "elemente_properties",
+ title="Element — Eigenschaften",
+ size=(480, 720),
+ bridge=b)
diff --git a/rhino/elemente_uebersicht.py b/rhino/elemente_uebersicht.py
new file mode 100644
index 0000000..5084f4f
--- /dev/null
+++ b/rhino/elemente_uebersicht.py
@@ -0,0 +1,186 @@
+#! python 3
+# -*- coding: utf-8 -*-
+"""
+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",
+ "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,
+ })
+ return {"geschosse": out_geschosse, "items": items}
+
+
+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)
+
+
+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)
diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx
index 39fca78..fb9f72c 100644
--- a/src/ElementeApp.jsx
+++ b/src/ElementeApp.jsx
@@ -1,13 +1,13 @@
import { useEffect, useRef, useState } from 'react'
import Icon from './components/Icon'
+import { BarToggle, BarButton } from './components/BarControls'
import {
onMessage, notifyReady,
- listElemente, createWall, createDecke, createDach,
+ createWall, createDecke, createDach,
createFenster, createTuer, createAussparung, createTreppe,
createStuetze, createTraeger, createRaum,
- exportRaeume,
openSwisstopo, openSwisstopoDialog, openOsmDialog,
- updateElement, deleteElement, regenerateAllElements,
+ updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
} from './lib/rhinoBridge'
const labelXs = {
@@ -29,17 +29,15 @@ function ReferenzSelector({ value, onChange }) {
{ code: 'right', label: 'Rechts', hint: 'Achse auf rechter Aussenseite' },
]
return (
-
+
{opts.map(o => (
- onChange(o.code)}
- className={value === o.code ? 'btn-contained' : 'btn-outlined'}
- style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
title={o.hint}
- >
- {o.label}
-
+ />
))}
)
@@ -213,10 +211,6 @@ function ElementList({ elements }) {
return (
Alle Elemente
@@ -328,7 +322,7 @@ function ElementListRow({ el, meta }) {
}
-function NeuesElementSection({ noGeschoss, activeName }) {
+function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
const [treppeMenuOpen, setTreppeMenuOpen] = useState(false)
const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false)
const [traegerMenuOpen, setTraegerMenuOpen] = useState(false)
@@ -380,13 +374,17 @@ function NeuesElementSection({ noGeschoss, activeName }) {
return (
-
Neues Element
+
0 ? ' · ' + elementsCount : ''}`}
+ onClick={() => openElementeUebersicht()}
+ disabled={elementsCount === 0}
+ title={elementsCount > 0
+ ? `Projektübersicht öffnen — ${elementsCount} Elemente`
+ : 'Noch keine Elemente vorhanden'} />
{noGeschoss ? 'Kein Geschoss aktiv' : 'auf'}
@@ -494,6 +492,39 @@ function NeuesElementSection({ noGeschoss, activeName }) {
}
+// PropertiesView: gemeinsame Komponente, rendert die passende Property-
+// Form je nach Element-Typ. Wiederverwendbar in Inline + Satellite-Window.
+export function PropertiesView({ selected, geschosse, materials, hatchPatterns }) {
+ if (!selected) return null
+ const upd = (p) => updateElement(selected.id, p)
+ const del = (label) => () => { if (window.confirm(`${label} löschen?`)) deleteElement(selected.id) }
+ if (selected.kind === 'wand')
+ return
+ if (selected.kind === 'decke')
+ return
+ if (selected.kind === 'dach')
+ return
+ if (selected.kind === 'treppe')
+ return
+ if (selected.kind === 'stuetze' || selected.kind === 'traeger') {
+ const lbl = (KIND_META[selected.kind] || {}).label || 'Element'
+ return
+ }
+ if (selected.kind === 'raum')
+ return
+ if (selected.kind === 'aussparung')
+ return
+ // fenster/tuer
+ return
+}
+
+
export default function ElementeApp() {
const [state, setState] = useState({
elements: [], geschosse: [], selection: null,
@@ -520,110 +551,29 @@ export default function ElementeApp() {
background: 'var(--bg-base)', color: 'var(--text-primary)',
fontFamily: 'var(--font)', fontSize: 11,
}}>
- {/* Header */}
-
- Elemente
- {elements.length}
- exportRaeume()}
- className="btn-icon-tonal"
- disabled={!elements.some(e => e.kind === 'raum')}
- title="Raumliste als CSV exportieren"
- >
-
-
- regenerateAllElements()}
- className="btn-icon-tonal"
- disabled={elements.length === 0}
- title="Alle Elemente neu generieren (z.B. nach Geschoss-Änderung)"
- >
-
-
- listElemente()}
- className="btn-icon-tonal"
- title="Aktualisieren"
- >
-
-
-
-
- {/* Element-Toolbar: kategorisierte Pills */}
+ {/* Bei Selektion: Properties OBEN, NeuesElement darunter.
+ Ohne Selektion: nur NeuesElement. Die volle Element-Liste
+ kommt jetzt aus der Projekt-Uebersicht (account_tree-Button). */}
+ {selected && (
+
+
+ openElementeProperties()}
+ title="Eigenschaften in eigenem Fenster öffnen" />
+
+
+
+ )}
-
-
- {/* Properties */}
- {selected ? (
- selected.kind === 'wand' ? (
-
updateElement(selected.id, p)}
- onDelete={() => { if (window.confirm('Wand löschen?')) deleteElement(selected.id) }} />
- ) : selected.kind === 'decke' ? (
- updateElement(selected.id, p)}
- onDelete={() => { if (window.confirm('Decke löschen?')) deleteElement(selected.id) }} />
- ) : selected.kind === 'dach' ? (
- updateElement(selected.id, p)}
- onDelete={() => { if (window.confirm('Dach löschen?')) deleteElement(selected.id) }} />
- ) : selected.kind === 'treppe' ? (
- updateElement(selected.id, p)}
- onDelete={() => { if (window.confirm('Treppe löschen?')) deleteElement(selected.id) }} />
- ) : (selected.kind === 'stuetze' || selected.kind === 'traeger') ? (
- updateElement(selected.id, p)}
- onDelete={() => {
- const lbl = (KIND_META[selected.kind] || {}).label || 'Element'
- if (window.confirm(`${lbl} löschen?`)) deleteElement(selected.id)
- }} />
- ) : selected.kind === 'raum' ? (
- updateElement(selected.id, p)}
- onDelete={() => { if (window.confirm('Raum löschen?')) deleteElement(selected.id) }} />
- ) : selected.kind === 'aussparung' ? (
- { if (window.confirm('Aussparung löschen?')) deleteElement(selected.id) }} />
- ) : (
- updateElement(selected.id, p)}
- onDelete={() => {
- const label = selected.kind === 'fenster' ? 'Fenster' : 'Tür'
- if (window.confirm(`${label} löschen?`)) deleteElement(selected.id)
- }} />
- )
- ) : (
-
-
-
Kein Element selektiert.
-
- Eine Wand-Achse oder Decken-Outline in Rhino auswählen.
-
-
- )}
-
- {/* Liste aller Elemente */}
- {elements.length > 0 && (
-
- )}
)
@@ -665,7 +615,7 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
- border: '1px solid var(--accent)',
+ border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
@@ -681,7 +631,7 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
-
+
Profil
onUpdate({ angle: v })} />
-
+
{isStuetze ? 'Höhe' : 'OK über UK'}
@@ -783,7 +733,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
- Geschoss
+ Geschoss
onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -792,7 +742,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
- Nummer
+ Nummer
setNummer(e.target.value)}
onBlur={() => {
@@ -804,7 +754,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
- Name
+ Name
setName(e.target.value)}
onBlur={() => {
@@ -817,7 +767,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
- Typ
+ Typ
onUpdate({ sia: e.target.value })}
title="SIA 416 Flaechenklassifikation"
@@ -832,7 +782,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
- Füllung
+ Füllung
onUpdate({ fuellung: e.target.value })}
title="Hatch-Pattern im Normalmodus. Bei aktivem SIA-Modus wird klassifizierten Raeumen automatisch Solid forciert."
@@ -847,7 +797,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
- Rundung
+ Rundung
onUpdate({ rundung: e.target.value })}
style={{ flex: 1, fontSize: 11 }}
@@ -862,24 +812,20 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
-
Ausrichtung
-
+
Ausrichtung
+
{RAUM_ALIGN.map(a => (
- onUpdate({ align: a.code })}
- className={(raum.align || 'mid') === a.code ? 'btn-contained' : 'btn-outlined'}
- style={{ flex: 1, padding: '3px 4px', fontSize: 10,
- display: 'flex', alignItems: 'center',
- justifyContent: 'center', gap: 3 }}
- title={a.label}>
-
-
+ title={a.label} />
))}
-
Texthöhe
+
Texthöhe
setTxtH(e.target.value)}
onBlur={() => {
@@ -912,7 +858,7 @@ function AussparungProperties({ aussp, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
- border: '1px solid var(--accent)',
+ border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
@@ -964,7 +910,7 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
- border: '1px solid var(--accent)',
+ border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
@@ -978,7 +924,7 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
- Geschoss
+ Geschoss
onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -987,25 +933,20 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
-
Aufbau
-
-
Aufbau
+
+ onUpdate({ layered: false })}
- className={!wall.layered ? 'btn-contained' : 'btn-outlined'}
- style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
- title="Eine homogene Wand-Schicht (Standard)">Solid
-
+ onUpdate({ layered: true })}
- className={wall.layered ? 'btn-contained' : 'btn-outlined'}
- style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
- title="Mehrere Schichten mit individuellen Dicken und Farben"
- >Mehrschichtig
+ title="Mehrere Schichten mit individuellen Dicken und Farben" />
{!wall.layered && (
-
Dicke
+
Dicke
setDicke(e.target.value)}
onBlur={() => {
@@ -1025,7 +966,7 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
)}
- Referenz
+ Referenz
onUpdate({ referenz: v })} />
@@ -1090,11 +1031,10 @@ function LayersEditor({ layers, onChange, materials }) {
onChange={(patch) => updateAt(i, patch)}
onRemove={() => removeAt(i)} />
))}
-
- + Schicht
-
+
+
+
)
}
@@ -1183,7 +1123,7 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
- border: '1px solid var(--accent)',
+ border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
@@ -1197,7 +1137,7 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
- Geschoss
+ Geschoss
onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -1206,7 +1146,7 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
-
Dicke
+
Dicke
setDicke(e.target.value)}
onBlur={() => {
@@ -1259,7 +1199,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
- border: '1px solid var(--accent)',
+ border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
@@ -1274,7 +1214,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
{/* Dach-Typ */}
- Typ
+ Typ
onUpdate({ dachTyp: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -1286,7 +1226,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
- Geschoss
+ Geschoss
onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -1295,7 +1235,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
- Dicke
+ Dicke
setDicke(e.target.value)}
onBlur={() => {
@@ -1308,7 +1248,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
-
Neigung
+
Neigung
setNeigung(e.target.value)}
onBlur={() => {
@@ -1323,7 +1263,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
{dachTyp === 'pult' && (
-
Traufe
@@ -1344,7 +1284,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
{dachTyp === 'mansarde' && (
<>
-
Variante
+
Variante
onUpdate({ dachVariante: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -1379,7 +1319,7 @@ function MansardeFields({ dach, onUpdate }) {
return (
<>
-
Steil
@@ -1395,7 +1335,7 @@ function MansardeFields({ dach, onUpdate }) {
°
-
Knick H
@@ -1577,7 +1517,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
- border: '1px solid var(--accent)',
+ border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
@@ -1591,7 +1531,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
- Start
+ Start
onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
@@ -1600,7 +1540,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
- Ziel
+ Ziel
{
@@ -1621,7 +1561,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
- Breite
+ Breite
setBreite(e.target.value)}
onBlur={() => {
@@ -1635,7 +1575,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
- Stufen
+ Stufen
setNStufen(e.target.value)}
onBlur={() => {
@@ -1649,15 +1589,13 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
-
Lage
-
+
Lage
+
{REF_OPTIONS.map(o => (
- onUpdate({ treppeReferenz: o.code })}
- className={ref === o.code ? 'btn-contained' : 'btn-outlined'}
- style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}>
- {o.label}
-
+ onUpdate({ treppeReferenz: o.code })} />
))}
@@ -1666,19 +1604,17 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
{/* Unterseite-Modus */}
-
Unten
-
+
{MODUS_OPTIONS.map(o => (
- onUpdate({ treppeModus: o.code })}
- className={modus === o.code ? 'btn-contained' : 'btn-outlined'}
- style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
- title={o.hint}>
- {o.label}
-
+ title={o.hint} />
))}
@@ -1686,7 +1622,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
{/* Lauf-Plattendicke (nur fuer flach + plattenrand relevant) */}
{modus !== 'massiv' && (
-
Platte
@@ -1782,7 +1718,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
- border: '1px solid var(--accent)',
+ border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
@@ -1796,7 +1732,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
- Breite
+ Breite
setBreite(e.target.value)}
onBlur={() => commit('breite', breite, setBreite, oeff.breite)}
@@ -1806,7 +1742,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
- Höhe
+ Höhe
setHoehe(e.target.value)}
onBlur={() => commit('hoehe', hoehe, setHoehe, oeff.hoehe)}
@@ -1816,7 +1752,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
-
@@ -1836,19 +1772,17 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{/* Referenz-Lage: wo sitzt der Klick-Punkt in der Oeffnung */}
-
Ref
-
+
{OEFF_REFERENZ_OPTIONS.map(o => (
- onUpdate({ oeffReferenz: o.code })}
- className={(oeff.oeffReferenz || 'mid') === o.code ? 'btn-contained' : 'btn-outlined'}
- style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
- title={o.hint}>
- {o.label}
-
+ title={o.hint} />
))}
@@ -1857,7 +1791,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{/* Rahmen-Profil (Breite × Tiefe) — beide Felder gleich breit */}
-
Rahmen
@@ -1879,19 +1813,17 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{/* Rahmen-Lage im Wandquerschnitt */}
-
Lage
-
+
{RAHMEN_POS_OPTIONS.map(o => (
- onUpdate({ rahmenPos: o.code })}
- className={rahmenPos === o.code ? 'btn-contained' : 'btn-outlined'}
- style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
- title={o.hint}>
- {o.label}
-
+ title={o.hint} />
))}
@@ -1899,18 +1831,16 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{/* Fluegel-Anzahl — nur fuer Fenster (Tueren haben ein einzelnes Tuerblatt) */}
{isFenster && (
-
Flügel
-
+
{[1, 2, 3, 4].map(n => (
- onUpdate({ fluegel: n })}
- className={fluegel === n ? 'btn-contained' : 'btn-outlined'}
- style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}>
- {n}
-
+ onUpdate({ fluegel: n })} />
))}
@@ -1920,7 +1850,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{isFenster && (
<>
-
Sims a.
@@ -1932,7 +1862,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
-
Sims i.
@@ -1948,13 +1878,11 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
{/* Glas-Toggle: bei Tueren ersetzt Glas das Tuerblatt (verglaste Tuer) */}
- onUpdate({ glas: !oeff.glas })}
- className={oeff.glas ? 'btn-contained' : 'btn-outlined'}
- style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
- title={isFenster ? 'Glasscheibe sichtbar' : 'Verglaste Tür (statt Türblatt)'}>
- {isFenster ? 'Glas' : 'Verglast'} {oeff.glas ? '✓' : ''}
-
+ title={isFenster ? 'Glasscheibe sichtbar' : 'Verglaste Tür (statt Türblatt)'} />
)
@@ -1965,13 +1893,10 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
function AutoOverrideField({ label, auto, autoValue, rawValue, onChangeRaw, onToggle, onCommit }) {
return (
-
{label}
-
- {auto ? 'Auto' : 'Custom'}
-
+
{label}
+
{
+ onMessage('STATE', (s) => setState(prev => ({ ...prev, ...s })))
+ notifyReady()
+ }, [])
+
+ const elements = state.elements || []
+ const selected = elements.find(el => el.id === state.selection)
+
+ return (
+
+
+ {selected ? (
+
+ ) : (
+
+
+
Kein Element selektiert.
+
Im Viewport ein Element wählen.
+
+ )}
+
+
+ )
+}
diff --git a/src/ElementeUebersichtApp.jsx b/src/ElementeUebersichtApp.jsx
new file mode 100644
index 0000000..e0d44ed
--- /dev/null
+++ b/src/ElementeUebersichtApp.jsx
@@ -0,0 +1,283 @@
+import { useEffect, useState, useMemo } from 'react'
+import Icon from './components/Icon'
+import { BarToggle, BarButton } from './components/BarControls'
+import { onMessage, notifyReady, send } from './lib/rhinoBridge'
+
+// BIM-artige Project-Tree-Ansicht: Geschoss → Kind → Element.
+// Klick → selektiert in Rhino. Shift-Klick → Zoom-to-Element.
+
+const KIND_ORDER = [
+ 'wand', 'decke', 'dach', 'fenster', 'tuer', 'aussparung',
+ 'treppe', 'stuetze', 'traeger', 'raum',
+]
+
+const KIND_META = {
+ wand: { icon: 'view_week', label: 'Wände', color: '#888888' },
+ decke: { icon: 'layers', label: 'Decken', color: '#605850' },
+ dach: { icon: 'roofing', label: 'Dächer', color: '#7a4a3a' },
+ fenster: { icon: 'window', label: 'Fenster', color: '#5080c8' },
+ tuer: { icon: 'sensor_door', label: 'Türen', color: '#5080c8' },
+ aussparung: { icon: 'rectangle', label: 'Aussparungen', color: '#a89070' },
+ treppe: { icon: 'stairs', label: 'Treppen', color: '#c87050' },
+ stuetze: { icon: 'square_foot', label: 'Stützen', color: '#c87050' },
+ traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#a87858' },
+ raum: { icon: 'crop_free', label: 'Räume', color: '#5fa896' },
+}
+
+
+export default function ElementeUebersichtApp() {
+ const [state, setState] = useState({ geschosse: [], items: [] })
+ const [expanded, setExpanded] = useState({}) // { 'g_id': true, 'g_id::kind': true }
+ const [filter, setFilter] = useState('') // text search
+ const [filterKind, setFilterKind] = useState('') // single kind filter
+
+ useEffect(() => {
+ onMessage('STATE', (s) => setState(s || { geschosse: [], items: [] }))
+ notifyReady()
+ }, [])
+
+ const items = state.items || []
+ const geschosse = state.geschosse || []
+
+ const filtered = useMemo(() => {
+ let r = items
+ if (filterKind) r = r.filter(it => it.kind === filterKind)
+ if (filter.trim()) {
+ const q = filter.toLowerCase()
+ r = r.filter(it =>
+ (it.name || '').toLowerCase().includes(q) ||
+ (it.info || '').toLowerCase().includes(q) ||
+ it.kind.toLowerCase().includes(q))
+ }
+ return r
+ }, [items, filter, filterKind])
+
+ // Pre-grouped: g_id -> kind -> [items]
+ const tree = useMemo(() => {
+ const out = {}
+ for (const it of filtered) {
+ const g = it.geschossId || '__keingeschoss__'
+ const k = it.kind
+ if (!out[g]) out[g] = {}
+ if (!out[g][k]) out[g][k] = []
+ out[g][k].push(it)
+ }
+ return out
+ }, [filtered])
+
+ // Counts per kind across all (unfiltered) items — für Filter-Chips
+ const kindCounts = useMemo(() => {
+ const m = {}
+ for (const it of items) m[it.kind] = (m[it.kind] || 0) + 1
+ return m
+ }, [items])
+
+ const toggle = (key) => setExpanded(s => ({ ...s, [key]: !s[key] }))
+ const expandAll = () => {
+ const next = {}
+ for (const g of geschosse) {
+ next[g.id] = true
+ for (const k of KIND_ORDER) {
+ if (tree[g.id]?.[k]) next[g.id + '::' + k] = true
+ }
+ }
+ setExpanded(next)
+ }
+ const collapseAll = () => setExpanded({})
+
+ const onSelect = (item, ev) => {
+ if (ev.shiftKey) {
+ send('ZOOM_TO_ELEMENT', { objectId: item.objectId })
+ } else {
+ send('SELECT_ELEMENT', { objectId: item.objectId })
+ }
+ }
+
+ const totalCount = items.length
+ const filteredCount = filtered.length
+
+ return (
+
+ {/* Toolbar */}
+
+
+ setFilter(e.target.value)}
+ style={{
+ flex: 1, fontSize: 11,
+ padding: '4px 10px',
+ background: 'var(--bg-input)',
+ border: '1px solid var(--border)',
+ borderRadius: 999,
+ color: 'var(--text-primary)',
+ outline: 'none',
+ }} />
+
+
+
+ {/* Kind-Filter Chips */}
+
+ 0 ? '· ' + totalCount : ''}`}
+ active={!filterKind}
+ onClick={() => setFilterKind('')} />
+ {KIND_ORDER.filter(k => kindCounts[k]).map(k => {
+ const meta = KIND_META[k]
+ return (
+ setFilterKind(filterKind === k ? '' : k)} />
+ )
+ })}
+
+
+
+ {/* Tree */}
+
+ {totalCount === 0 ? (
+
+
+
Keine Elemente im Projekt.
+
Wände, Decken, Türen etc. via Elemente-Panel anlegen.
+
+ ) : (
+ geschosse.map(g => {
+ const groupForG = tree[g.id] || {}
+ const total = Object.values(groupForG).reduce((s, arr) => s + arr.length, 0)
+ if (total === 0) return null
+ const gOpen = expanded[g.id] !== false // default: open
+ return (
+
+
toggle(g.id)}
+ style={{
+ display: 'flex', alignItems: 'center', gap: 6,
+ padding: '6px 10px',
+ background: 'var(--bg-section)',
+ borderBottom: '1px solid var(--border)',
+ cursor: 'pointer', userSelect: 'none',
+ }}>
+
+ {g.name}
+ {g.okff != null && (
+ +{g.okff.toFixed(2)} m
+ )}
+
+ {total}
+
+ {gOpen && KIND_ORDER.map(k => {
+ const arr = groupForG[k]
+ if (!arr || arr.length === 0) return null
+ const meta = KIND_META[k]
+ const kKey = g.id + '::' + k
+ const kOpen = expanded[kKey] !== false // default: open
+ return (
+
+
toggle(kKey)}
+ style={{
+ display: 'flex', alignItems: 'center', gap: 5,
+ padding: '3px 14px',
+ background: 'var(--bg-item)',
+ borderBottom: '1px solid var(--border-light)',
+ cursor: 'pointer', userSelect: 'none',
+ }}>
+
+
+ {meta.label}
+
+ {arr.length}
+
+ {kOpen && arr.map((it, idx) => (
+
onSelect(it, ev)}
+ title="Klick: selektieren · Shift+Klick: zoomen"
+ style={{
+ display: 'flex', alignItems: 'center', gap: 8,
+ padding: '2px 14px 2px 30px',
+ background: it.selected ? 'var(--active-dim)' : 'transparent',
+ borderBottom: '1px solid var(--border-light)',
+ cursor: 'pointer',
+ minHeight: 22,
+ }}
+ onMouseEnter={(e) => {
+ if (!it.selected) e.currentTarget.style.background = 'var(--bg-item-hover)'
+ }}
+ onMouseLeave={(e) => {
+ if (!it.selected) e.currentTarget.style.background = 'transparent'
+ }}>
+ {String(idx + 1).padStart(2, '0')}
+ {it.name || meta.label}
+ {it.info}
+
+ ))}
+
+ )
+ })}
+
+ )
+ })
+ )}
+
+
+ {/* Footer */}
+
+ {filteredCount} {filteredCount !== totalCount && `von ${totalCount}`} Elemente
+
+
+ Klick = selektieren · Shift+Klick = zoomen
+
+
+
+ )
+}
diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js
index a49e50b..8ce8509 100644
--- a/src/lib/rhinoBridge.js
+++ b/src/lib/rhinoBridge.js
@@ -170,6 +170,11 @@ export function setOverridesPreset(name) { send('SET_OVERRIDES_PRESET', { name:
export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) }
export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) }
+export function openElementeUebersicht() { send('OPEN_ELEMENTE_UEBERSICHT', {}) }
+export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {}) }
+export function setSectionStyle(enabled, source, color, pattern, scale, rotation) {
+ send('SET_SECTION_STYLE', { enabled, source, color, pattern, scale, rotation })
+}
export function openAbout() { send('OPEN_ABOUT', {}) }
export function createText() { send('CREATE_TEXT', {}) }
export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) }
diff --git a/src/main.jsx b/src/main.jsx
index c2af898..096cdfc 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -24,6 +24,8 @@ import OverridesApp from './OverridesApp.jsx'
import DimensionenApp from './DimensionenApp.jsx'
import LayoutsApp from './LayoutsApp.jsx'
import ElementeApp from './ElementeApp.jsx'
+import ElementeUebersichtApp from './ElementeUebersichtApp.jsx'
+import ElementePropertiesApp from './ElementePropertiesApp.jsx'
const mode = (typeof window !== 'undefined' && window.PANEL_MODE) || 'ebenen'
const RootApp = mode === 'gestaltung' ? GestaltungApp
@@ -48,6 +50,8 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
: mode === 'masse_settings' ? MasseSettingsApp
: mode === 'about' ? AboutApp
: mode === 'text_editor' ? TextEditorApp
+ : mode === 'elemente_uebersicht' ? ElementeUebersichtApp
+ : mode === 'elemente_properties' ? ElementePropertiesApp
: App
window.onerror = function (msg, src, line, col, err) {