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 => ( - + /> ))}
) @@ -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} - - - -
-
- {/* 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
@@ -783,7 +733,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
- Geschoss + Geschoss 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({ 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 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 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)} /> ))} - +
+ +
) } @@ -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 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({ 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({ geschoss: e.target.value })} style={{ flex: 1, fontSize: 11 }}> @@ -1600,7 +1540,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
- Ziel + Ziel 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 })} /> ))}
@@ -1666,19 +1604,17 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) { {/* Unterseite-Modus */}
- Unten -
+
{MODUS_OPTIONS.map(o => ( - + 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 => ( - + 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 => ( - + 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 })} /> ))}
@@ -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) */}
- + 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} - + {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) {