Compare commits
2 Commits
d5bcee2157
...
9ae8574ab0
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ae8574ab0 | |||
| 15fb0a6037 |
+23
-2
@@ -4684,6 +4684,20 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
elif t == "DELETE_WALL": self._delete_wall(p.get("id"))
|
elif t == "DELETE_WALL": self._delete_wall(p.get("id"))
|
||||||
elif t == "DELETE_ELEMENT": 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 == "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):
|
def _notify_active_geschoss(self):
|
||||||
"""Schlanker Partial-Push: nur activeGeschoss + activeGeschossName.
|
"""Schlanker Partial-Push: nur activeGeschoss + activeGeschossName.
|
||||||
@@ -4884,7 +4898,7 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
})
|
})
|
||||||
elements.append(base)
|
elements.append(base)
|
||||||
sel_id = next((e["id"] for e in elements if e["selected"]), None)
|
sel_id = next((e["id"] for e in elements if e["selected"]), None)
|
||||||
self.send("STATE", {
|
payload = {
|
||||||
"elements": elements,
|
"elements": elements,
|
||||||
"geschosse": [{"id": g.get("id"), "name": g.get("name")}
|
"geschosse": [{"id": g.get("id"), "name": g.get("name")}
|
||||||
for g in geschosse if isinstance(g, dict)],
|
for g in geschosse if isinstance(g, dict)],
|
||||||
@@ -4897,7 +4911,14 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
{"name": n, "color": m["color"],
|
{"name": n, "color": m["color"],
|
||||||
"hatch": m.get("hatch", ""), "scale": m.get("scale", 1.0)}
|
"hatch": m.get("hatch", ""), "scale": m.get("scale", 1.0)}
|
||||||
for n, m in _MATERIAL_LIBRARY.items()],
|
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 -------------------------------------------------------
|
# --- Wand-Befehle -------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -853,6 +853,18 @@ def _selection_summary(doc):
|
|||||||
fill_scales = set()
|
fill_scales = set()
|
||||||
fill_rots = set()
|
fill_rots = set()
|
||||||
has_closed_curves = False
|
has_closed_curves = False
|
||||||
|
# Section-Style (3D)
|
||||||
|
sec_enabled = set()
|
||||||
|
sec_sources = set()
|
||||||
|
sec_colors = set()
|
||||||
|
sec_patterns = set()
|
||||||
|
sec_scales = set()
|
||||||
|
sec_rots = set()
|
||||||
|
# Geometry-Kind-Klassifikation: 'curve' (closed planar 2D), 'curveOpen'
|
||||||
|
# (offene Kurve), '3d' (Brep/Extrusion/Mesh — Volumen mit Schnittflaeche),
|
||||||
|
# 'other'. Aggregiert ueber alle Selektions-Objekte zu kind=
|
||||||
|
# 'curve' / '3d' / 'mixed' / 'other'.
|
||||||
|
geometry_kinds = set()
|
||||||
|
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
a = obj.Attributes
|
a = obj.Attributes
|
||||||
@@ -895,6 +907,72 @@ def _selection_summary(doc):
|
|||||||
nm = _safe_layer_label(doc, layer, a.LayerIndex)
|
nm = _safe_layer_label(doc, layer, a.LayerIndex)
|
||||||
layer_names.add(nm)
|
layer_names.add(nm)
|
||||||
|
|
||||||
|
# Geometry-Klassifikation. DOSSIER-Source-Curves (wand_axis,
|
||||||
|
# decke_outline, ...) sind Meta-Geometrie und keine User-facing
|
||||||
|
# Form — fuer den Section/Fill-Entscheid ignorieren. Dann wird
|
||||||
|
# eine Wand-Selektion (Achse + Volume) als reines 3D klassifiziert.
|
||||||
|
g = obj.Geometry
|
||||||
|
is_3d = isinstance(g, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD))
|
||||||
|
dossier_type = ""
|
||||||
|
try: dossier_type = a.GetUserString("dossier_element_type") or ""
|
||||||
|
except Exception: pass
|
||||||
|
is_dossier_source = dossier_type.endswith(("_axis", "_outline", "_point"))
|
||||||
|
if isinstance(g, rg.Curve) and not is_dossier_source:
|
||||||
|
geometry_kinds.add('curve' if (g.IsClosed and g.IsPlanar()) else 'curveOpen')
|
||||||
|
elif is_3d:
|
||||||
|
geometry_kinds.add('3d')
|
||||||
|
elif isinstance(g, rg.Curve) and is_dossier_source:
|
||||||
|
pass # ignorieren — Volume zaehlt fuer die Klassifikation
|
||||||
|
else:
|
||||||
|
geometry_kinds.add('other')
|
||||||
|
|
||||||
|
# Section-Style aus Object-Attributes lesen (Rhino 8, mit Fallbacks
|
||||||
|
# fuer Property-Namen die je nach Build variieren).
|
||||||
|
if is_3d:
|
||||||
|
src_attr = None
|
||||||
|
try:
|
||||||
|
src_attr = getattr(a, "SectionAttributesSource", None)
|
||||||
|
except Exception: src_attr = None
|
||||||
|
if src_attr is not None:
|
||||||
|
try:
|
||||||
|
src_name = str(src_attr).lower()
|
||||||
|
if "layer" in src_name: sec_sources.add("layer")
|
||||||
|
elif "object" in src_name: sec_sources.add("object")
|
||||||
|
except Exception: pass
|
||||||
|
# Hatch-Index/Scale/Rotation
|
||||||
|
hidx = None
|
||||||
|
for n in ("SectionHatchIndex", "HatchPatternIndex"):
|
||||||
|
if hasattr(a, n):
|
||||||
|
try:
|
||||||
|
v = getattr(a, n)
|
||||||
|
if v is not None: hidx = int(v); break
|
||||||
|
except Exception: pass
|
||||||
|
if hidx is not None and hidx >= 0 and hidx < doc.HatchPatterns.Count:
|
||||||
|
sec_enabled.add(True)
|
||||||
|
try: sec_patterns.add(doc.HatchPatterns[hidx].Name)
|
||||||
|
except Exception: pass
|
||||||
|
elif hidx == -1:
|
||||||
|
sec_enabled.add(False)
|
||||||
|
for n, target in (
|
||||||
|
(("SectionHatchScale", "HatchPatternScale"), sec_scales),
|
||||||
|
(("SectionHatchRotation", "HatchPatternRotation"), sec_rots),
|
||||||
|
):
|
||||||
|
for nn in n:
|
||||||
|
if hasattr(a, nn):
|
||||||
|
try:
|
||||||
|
v = float(getattr(a, nn))
|
||||||
|
target.add(round(v, 4)
|
||||||
|
if target is sec_scales
|
||||||
|
else round(math.degrees(v), 2))
|
||||||
|
break
|
||||||
|
except Exception: pass
|
||||||
|
for n in ("SectionFillColor", "SectionHatchColor", "HatchColor"):
|
||||||
|
if hasattr(a, n):
|
||||||
|
try:
|
||||||
|
c = _color_to_hex(getattr(a, n))
|
||||||
|
if c: sec_colors.add(c); break
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
# Fuellung
|
# Fuellung
|
||||||
if _is_closed_planar_curve(obj.Geometry):
|
if _is_closed_planar_curve(obj.Geometry):
|
||||||
has_closed_curves = True
|
has_closed_curves = True
|
||||||
@@ -987,6 +1065,18 @@ def _selection_summary(doc):
|
|||||||
"layerLinetype": single(layer_lts),
|
"layerLinetype": single(layer_lts),
|
||||||
"layerName": single(layer_names),
|
"layerName": single(layer_names),
|
||||||
"canFill": has_closed_curves,
|
"canFill": has_closed_curves,
|
||||||
|
# Section-Style (3D)
|
||||||
|
"sectionEnabled": single(sec_enabled),
|
||||||
|
"sectionSource": single(sec_sources),
|
||||||
|
"sectionColor": single(sec_colors),
|
||||||
|
"sectionPattern": single(sec_patterns),
|
||||||
|
"sectionScale": single(sec_scales),
|
||||||
|
"sectionRotation": single(sec_rots),
|
||||||
|
# geometryKind: 'curve' | 'curveOpen' | '3d' | 'mixed' | 'other'
|
||||||
|
"geometryKind": (
|
||||||
|
'mixed' if len(geometry_kinds & {'curve', 'curveOpen', '3d'}) > 1
|
||||||
|
else (next(iter(geometry_kinds)) if len(geometry_kinds) == 1 else 'other')
|
||||||
|
),
|
||||||
"fillEnabled": single(fill_enabled),
|
"fillEnabled": single(fill_enabled),
|
||||||
"fillColor": single(fill_colors),
|
"fillColor": single(fill_colors),
|
||||||
"fillSource": single(fill_sources),
|
"fillSource": single(fill_sources),
|
||||||
@@ -1061,6 +1151,15 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
p.get("scale"),
|
p.get("scale"),
|
||||||
p.get("rotation"),
|
p.get("rotation"),
|
||||||
)
|
)
|
||||||
|
elif t == "SET_SECTION_STYLE":
|
||||||
|
self._set_section_style(
|
||||||
|
bool(p.get("enabled")),
|
||||||
|
p.get("source", "object"),
|
||||||
|
p.get("color"),
|
||||||
|
p.get("pattern"),
|
||||||
|
p.get("scale"),
|
||||||
|
p.get("rotation"),
|
||||||
|
)
|
||||||
|
|
||||||
def _send_selection(self):
|
def _send_selection(self):
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
@@ -1376,6 +1475,79 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
self._send_selection()
|
self._send_selection()
|
||||||
|
|
||||||
|
# ---- SectionStyle (per-Object, Rhino 8) -------------------------------
|
||||||
|
|
||||||
|
def _set_section_style(self, enabled, source, color_hex,
|
||||||
|
pattern_name=None, scale=None, rotation_deg=None):
|
||||||
|
"""Setzt Per-Object SectionStyle-Properties auf die selektierten
|
||||||
|
3D-Objekte. Rhino 8 expone diese Properties auf ObjectAttributes
|
||||||
|
unter teils variierenden Namen — wir versuchen die bekannten."""
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
|
is_layer_source = (source == "layer")
|
||||||
|
|
||||||
|
# Hatch-Pattern-Index ermitteln
|
||||||
|
pat_idx = -1
|
||||||
|
if pattern_name and pattern_name not in ("None", ""):
|
||||||
|
try: pat_idx = doc.HatchPatterns.Find(pattern_name, True)
|
||||||
|
except Exception: pat_idx = -1
|
||||||
|
if pat_idx < 0 and pattern_name not in ("None", ""):
|
||||||
|
try: pat_idx = doc.HatchPatterns.Find("Solid", True)
|
||||||
|
except Exception: pat_idx = -1
|
||||||
|
|
||||||
|
col = _hex_to_color(color_hex) if color_hex else None
|
||||||
|
scale_v = float(scale) if scale is not None else 1.0
|
||||||
|
rot_rad = math.radians(float(rotation_deg)) if rotation_deg is not None else 0.0
|
||||||
|
|
||||||
|
def _try_set_attr(a, names, value):
|
||||||
|
for n in names:
|
||||||
|
if hasattr(a, n):
|
||||||
|
try:
|
||||||
|
setattr(a, n, value)
|
||||||
|
return n
|
||||||
|
except Exception: pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
n_ok = 0
|
||||||
|
for obj in objs:
|
||||||
|
geom = obj.Geometry
|
||||||
|
if not isinstance(geom, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD)):
|
||||||
|
continue
|
||||||
|
a = obj.Attributes.Duplicate()
|
||||||
|
|
||||||
|
# Source: FromLayer vs FromObject — verschiedene Enum-Namen
|
||||||
|
if is_layer_source:
|
||||||
|
# Versuche SectionAttributesSource auf FromLayer
|
||||||
|
_try_set_attr(a, ("SectionAttributesSource",),
|
||||||
|
Rhino.DocObjects.SectionAttributesSource.FromLayer
|
||||||
|
if hasattr(Rhino.DocObjects, "SectionAttributesSource") else 0)
|
||||||
|
else:
|
||||||
|
_try_set_attr(a, ("SectionAttributesSource",),
|
||||||
|
Rhino.DocObjects.SectionAttributesSource.FromObject
|
||||||
|
if hasattr(Rhino.DocObjects, "SectionAttributesSource") else 1)
|
||||||
|
|
||||||
|
if not enabled or pattern_name == "None":
|
||||||
|
# Hatch-Index auf -1 = keine Fuellung
|
||||||
|
_try_set_attr(a, ("SectionHatchIndex", "HatchPatternIndex"), -1)
|
||||||
|
else:
|
||||||
|
if pat_idx >= 0:
|
||||||
|
_try_set_attr(a, ("SectionHatchIndex", "HatchPatternIndex"), pat_idx)
|
||||||
|
_try_set_attr(a, ("SectionHatchScale", "HatchPatternScale"), scale_v)
|
||||||
|
_try_set_attr(a, ("SectionHatchRotation", "HatchPatternRotation"), rot_rad)
|
||||||
|
if col is not None:
|
||||||
|
_try_set_attr(a, ("SectionFillColor", "SectionHatchColor",
|
||||||
|
"HatchColor"), col)
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc.Objects.ModifyAttributes(obj, a, True)
|
||||||
|
n_ok += 1
|
||||||
|
except Exception as ex:
|
||||||
|
print("[GESTALTUNG] SectionStyle ModifyAttributes:", ex)
|
||||||
|
|
||||||
|
print("[GESTALTUNG] SectionStyle auf {} Objekt(e) appliziert".format(n_ok))
|
||||||
|
doc.Views.Redraw()
|
||||||
|
self._send_selection()
|
||||||
|
|
||||||
|
|
||||||
# --- Selection-Events ----------------------------------------------------
|
# --- Selection-Events ----------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
+164
-239
@@ -1,13 +1,13 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import Icon from './components/Icon'
|
import Icon from './components/Icon'
|
||||||
|
import { BarToggle, BarButton } from './components/BarControls'
|
||||||
import {
|
import {
|
||||||
onMessage, notifyReady,
|
onMessage, notifyReady,
|
||||||
listElemente, createWall, createDecke, createDach,
|
createWall, createDecke, createDach,
|
||||||
createFenster, createTuer, createAussparung, createTreppe,
|
createFenster, createTuer, createAussparung, createTreppe,
|
||||||
createStuetze, createTraeger, createRaum,
|
createStuetze, createTraeger, createRaum,
|
||||||
exportRaeume,
|
|
||||||
openSwisstopo, openSwisstopoDialog, openOsmDialog,
|
openSwisstopo, openSwisstopoDialog, openOsmDialog,
|
||||||
updateElement, deleteElement, regenerateAllElements,
|
updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const labelXs = {
|
const labelXs = {
|
||||||
@@ -29,17 +29,15 @@ function ReferenzSelector({ value, onChange }) {
|
|||||||
{ code: 'right', label: 'Rechts', hint: 'Achse auf rechter Aussenseite' },
|
{ code: 'right', label: 'Rechts', hint: 'Achse auf rechter Aussenseite' },
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
|
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
|
||||||
{opts.map(o => (
|
{opts.map(o => (
|
||||||
<button
|
<BarToggle
|
||||||
key={o.code}
|
key={o.code}
|
||||||
|
label={o.label}
|
||||||
|
active={value === o.code}
|
||||||
onClick={() => onChange(o.code)}
|
onClick={() => onChange(o.code)}
|
||||||
className={value === o.code ? 'btn-contained' : 'btn-outlined'}
|
|
||||||
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
|
|
||||||
title={o.hint}
|
title={o.hint}
|
||||||
>
|
/>
|
||||||
{o.label}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -213,10 +211,6 @@ function ElementList({ elements }) {
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column', gap: 8,
|
display: 'flex', flexDirection: 'column', gap: 8,
|
||||||
padding: 10,
|
|
||||||
background: 'var(--bg-section)',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 'var(--r-lg)',
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<span style={{ ...labelXs, flex: 1 }}>Alle Elemente</span>
|
<span style={{ ...labelXs, flex: 1 }}>Alle Elemente</span>
|
||||||
@@ -328,7 +322,7 @@ function ElementListRow({ el, meta }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function NeuesElementSection({ noGeschoss, activeName }) {
|
function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
|
||||||
const [treppeMenuOpen, setTreppeMenuOpen] = useState(false)
|
const [treppeMenuOpen, setTreppeMenuOpen] = useState(false)
|
||||||
const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false)
|
const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false)
|
||||||
const [traegerMenuOpen, setTraegerMenuOpen] = useState(false)
|
const [traegerMenuOpen, setTraegerMenuOpen] = useState(false)
|
||||||
@@ -380,13 +374,17 @@ function NeuesElementSection({ noGeschoss, activeName }) {
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column', gap: 10,
|
display: 'flex', flexDirection: 'column', gap: 10,
|
||||||
padding: 10, marginBottom: 8,
|
marginBottom: 8,
|
||||||
background: 'var(--bg-section)',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 'var(--r-lg)',
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 10 }}>
|
||||||
<span style={labelXs}>Neues Element</span>
|
<BarToggle
|
||||||
|
icon="account_tree"
|
||||||
|
label={`Projektübersicht${elementsCount > 0 ? ' · ' + elementsCount : ''}`}
|
||||||
|
onClick={() => openElementeUebersicht()}
|
||||||
|
disabled={elementsCount === 0}
|
||||||
|
title={elementsCount > 0
|
||||||
|
? `Projektübersicht öffnen — ${elementsCount} Elemente`
|
||||||
|
: 'Noch keine Elemente vorhanden'} />
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
<span style={{ color: noGeschoss ? 'var(--danger)' : 'var(--text-muted)' }}>
|
<span style={{ color: noGeschoss ? 'var(--danger)' : 'var(--text-muted)' }}>
|
||||||
{noGeschoss ? 'Kein Geschoss aktiv' : 'auf'}
|
{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 <WallProperties wall={selected} geschosse={geschosse} materials={materials || []}
|
||||||
|
onUpdate={upd} onDelete={del('Wand')} />
|
||||||
|
if (selected.kind === 'decke')
|
||||||
|
return <DeckenProperties decke={selected} geschosse={geschosse}
|
||||||
|
onUpdate={upd} onDelete={del('Decke')} />
|
||||||
|
if (selected.kind === 'dach')
|
||||||
|
return <DachProperties dach={selected} geschosse={geschosse}
|
||||||
|
onUpdate={upd} onDelete={del('Dach')} />
|
||||||
|
if (selected.kind === 'treppe')
|
||||||
|
return <TreppeProperties treppe={selected} geschosse={geschosse}
|
||||||
|
onUpdate={upd} onDelete={del('Treppe')} />
|
||||||
|
if (selected.kind === 'stuetze' || selected.kind === 'traeger') {
|
||||||
|
const lbl = (KIND_META[selected.kind] || {}).label || 'Element'
|
||||||
|
return <TragwerkProperties el={selected} onUpdate={upd} onDelete={del(lbl)} />
|
||||||
|
}
|
||||||
|
if (selected.kind === 'raum')
|
||||||
|
return <RaumProperties raum={selected} geschosse={geschosse}
|
||||||
|
hatchPatterns={hatchPatterns} onUpdate={upd} onDelete={del('Raum')} />
|
||||||
|
if (selected.kind === 'aussparung')
|
||||||
|
return <AussparungProperties aussp={selected} onDelete={del('Aussparung')} />
|
||||||
|
// fenster/tuer
|
||||||
|
return <OeffnungProperties oeff={selected} onUpdate={upd}
|
||||||
|
onDelete={del(selected.kind === 'fenster' ? 'Fenster' : 'Tür')} />
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function ElementeApp() {
|
export default function ElementeApp() {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
elements: [], geschosse: [], selection: null,
|
elements: [], geschosse: [], selection: null,
|
||||||
@@ -520,110 +551,29 @@ export default function ElementeApp() {
|
|||||||
background: 'var(--bg-base)', color: 'var(--text-primary)',
|
background: 'var(--bg-base)', color: 'var(--text-primary)',
|
||||||
fontFamily: 'var(--font)', fontSize: 11,
|
fontFamily: 'var(--font)', fontSize: 11,
|
||||||
}}>
|
}}>
|
||||||
{/* Header */}
|
|
||||||
<div style={{
|
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
|
||||||
padding: '8px 10px', borderBottom: '1px solid var(--border)',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}>
|
|
||||||
<span style={{ flex: 1, fontWeight: 600 }}>Elemente</span>
|
|
||||||
<span className="chip" style={{ fontSize: 8 }}>{elements.length}</span>
|
|
||||||
<button
|
|
||||||
onClick={() => exportRaeume()}
|
|
||||||
className="btn-icon-tonal"
|
|
||||||
disabled={!elements.some(e => e.kind === 'raum')}
|
|
||||||
title="Raumliste als CSV exportieren"
|
|
||||||
>
|
|
||||||
<Icon name="download" size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => regenerateAllElements()}
|
|
||||||
className="btn-icon-tonal"
|
|
||||||
disabled={elements.length === 0}
|
|
||||||
title="Alle Elemente neu generieren (z.B. nach Geschoss-Änderung)"
|
|
||||||
>
|
|
||||||
<Icon name="sync" size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => listElemente()}
|
|
||||||
className="btn-icon-tonal"
|
|
||||||
title="Aktualisieren"
|
|
||||||
>
|
|
||||||
<Icon name="refresh" size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 8 }}>
|
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 8 }}>
|
||||||
{/* 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 && (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div style={{ position: 'absolute', top: 8, right: 38, zIndex: 1 }}>
|
||||||
|
<BarButton icon="open_in_new"
|
||||||
|
onClick={() => openElementeProperties()}
|
||||||
|
title="Eigenschaften in eigenem Fenster öffnen" />
|
||||||
|
</div>
|
||||||
|
<PropertiesView
|
||||||
|
selected={selected}
|
||||||
|
geschosse={geschosse}
|
||||||
|
materials={state.materials || []}
|
||||||
|
hatchPatterns={state.hatchPatterns} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<NeuesElementSection
|
<NeuesElementSection
|
||||||
noGeschoss={noGeschoss}
|
noGeschoss={noGeschoss}
|
||||||
activeName={activeName}
|
activeName={activeName}
|
||||||
|
elementsCount={elements.length}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
{/* Properties */}
|
|
||||||
{selected ? (
|
|
||||||
selected.kind === 'wand' ? (
|
|
||||||
<WallProperties wall={selected} geschosse={geschosse}
|
|
||||||
materials={state.materials || []}
|
|
||||||
onUpdate={(p) => updateElement(selected.id, p)}
|
|
||||||
onDelete={() => { if (window.confirm('Wand löschen?')) deleteElement(selected.id) }} />
|
|
||||||
) : selected.kind === 'decke' ? (
|
|
||||||
<DeckenProperties decke={selected} geschosse={geschosse}
|
|
||||||
onUpdate={(p) => updateElement(selected.id, p)}
|
|
||||||
onDelete={() => { if (window.confirm('Decke löschen?')) deleteElement(selected.id) }} />
|
|
||||||
) : selected.kind === 'dach' ? (
|
|
||||||
<DachProperties dach={selected} geschosse={geschosse}
|
|
||||||
onUpdate={(p) => updateElement(selected.id, p)}
|
|
||||||
onDelete={() => { if (window.confirm('Dach löschen?')) deleteElement(selected.id) }} />
|
|
||||||
) : selected.kind === 'treppe' ? (
|
|
||||||
<TreppeProperties treppe={selected} geschosse={geschosse}
|
|
||||||
onUpdate={(p) => updateElement(selected.id, p)}
|
|
||||||
onDelete={() => { if (window.confirm('Treppe löschen?')) deleteElement(selected.id) }} />
|
|
||||||
) : (selected.kind === 'stuetze' || selected.kind === 'traeger') ? (
|
|
||||||
<TragwerkProperties el={selected}
|
|
||||||
onUpdate={(p) => 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' ? (
|
|
||||||
<RaumProperties raum={selected} geschosse={geschosse}
|
|
||||||
hatchPatterns={state.hatchPatterns}
|
|
||||||
onUpdate={(p) => updateElement(selected.id, p)}
|
|
||||||
onDelete={() => { if (window.confirm('Raum löschen?')) deleteElement(selected.id) }} />
|
|
||||||
) : selected.kind === 'aussparung' ? (
|
|
||||||
<AussparungProperties aussp={selected}
|
|
||||||
onDelete={() => { if (window.confirm('Aussparung löschen?')) deleteElement(selected.id) }} />
|
|
||||||
) : (
|
|
||||||
<OeffnungProperties oeff={selected}
|
|
||||||
onUpdate={(p) => updateElement(selected.id, p)}
|
|
||||||
onDelete={() => {
|
|
||||||
const label = selected.kind === 'fenster' ? 'Fenster' : 'Tür'
|
|
||||||
if (window.confirm(`${label} löschen?`)) deleteElement(selected.id)
|
|
||||||
}} />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div style={{
|
|
||||||
padding: '20px 16px', textAlign: 'center',
|
|
||||||
color: 'var(--text-muted)', fontSize: 11,
|
|
||||||
border: '1px dashed var(--border)',
|
|
||||||
borderRadius: 'var(--r-lg)',
|
|
||||||
background: 'var(--bg-section)',
|
|
||||||
marginBottom: 8,
|
|
||||||
}}>
|
|
||||||
<Icon name="touch_app" size={24} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
|
||||||
<div style={{ marginTop: 6 }}>Kein Element selektiert.</div>
|
|
||||||
<div style={{ marginTop: 4, fontSize: 10 }}>
|
|
||||||
Eine Wand-Achse oder Decken-Outline in Rhino auswählen.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Liste aller Elemente */}
|
|
||||||
{elements.length > 0 && (
|
|
||||||
<ElementList elements={elements} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -665,7 +615,7 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
|
|||||||
display: 'flex', flexDirection: 'column', gap: 8,
|
display: 'flex', flexDirection: 'column', gap: 8,
|
||||||
padding: 10, marginBottom: 8,
|
padding: 10, marginBottom: 8,
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
border: '1px solid var(--accent)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 'var(--r-lg)',
|
borderRadius: 'var(--r-lg)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
@@ -681,7 +631,7 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>
|
||||||
Profil
|
Profil
|
||||||
</span>
|
</span>
|
||||||
<select value={profil}
|
<select value={profil}
|
||||||
@@ -715,7 +665,7 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
|
|||||||
onCommit={(v) => onUpdate({ angle: v })} />
|
onCommit={(v) => onUpdate({ angle: v })} />
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>
|
||||||
{isStuetze ? 'Höhe' : 'OK über UK'}
|
{isStuetze ? 'Höhe' : 'OK über UK'}
|
||||||
</span>
|
</span>
|
||||||
<input type="text" value={zRaw}
|
<input type="text" value={zRaw}
|
||||||
@@ -767,7 +717,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
|
|||||||
display: 'flex', flexDirection: 'column', gap: 8,
|
display: 'flex', flexDirection: 'column', gap: 8,
|
||||||
padding: 10, marginBottom: 8,
|
padding: 10, marginBottom: 8,
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
border: '1px solid var(--accent)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 'var(--r-lg)',
|
borderRadius: 'var(--r-lg)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
@@ -783,7 +733,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Geschoss</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Geschoss</span>
|
||||||
<select value={raum.geschoss}
|
<select value={raum.geschoss}
|
||||||
onChange={(e) => onUpdate({ geschoss: e.target.value })}
|
onChange={(e) => onUpdate({ geschoss: e.target.value })}
|
||||||
style={{ flex: 1, fontSize: 11 }}>
|
style={{ flex: 1, fontSize: 11 }}>
|
||||||
@@ -792,7 +742,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Nummer</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Nummer</span>
|
||||||
<input type="text" value={nummer}
|
<input type="text" value={nummer}
|
||||||
onChange={(e) => setNummer(e.target.value)}
|
onChange={(e) => setNummer(e.target.value)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@@ -804,7 +754,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Name</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Name</span>
|
||||||
<input type="text" value={name}
|
<input type="text" value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@@ -817,7 +767,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Typ</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Typ</span>
|
||||||
<select value={raum.sia || ''}
|
<select value={raum.sia || ''}
|
||||||
onChange={(e) => onUpdate({ sia: e.target.value })}
|
onChange={(e) => onUpdate({ sia: e.target.value })}
|
||||||
title="SIA 416 Flaechenklassifikation"
|
title="SIA 416 Flaechenklassifikation"
|
||||||
@@ -832,7 +782,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Füllung</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Füllung</span>
|
||||||
<select value={fuell}
|
<select value={fuell}
|
||||||
onChange={(e) => onUpdate({ fuellung: e.target.value })}
|
onChange={(e) => onUpdate({ fuellung: e.target.value })}
|
||||||
title="Hatch-Pattern im Normalmodus. Bei aktivem SIA-Modus wird klassifizierten Raeumen automatisch Solid forciert."
|
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 })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Rundung</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Rundung</span>
|
||||||
<select value={raum.rundung || ''}
|
<select value={raum.rundung || ''}
|
||||||
onChange={(e) => onUpdate({ rundung: e.target.value })}
|
onChange={(e) => onUpdate({ rundung: e.target.value })}
|
||||||
style={{ flex: 1, fontSize: 11 }}
|
style={{ flex: 1, fontSize: 11 }}
|
||||||
@@ -862,24 +812,20 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Ausrichtung</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Ausrichtung</span>
|
||||||
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
|
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
|
||||||
{RAUM_ALIGN.map(a => (
|
{RAUM_ALIGN.map(a => (
|
||||||
<button key={a.code}
|
<BarToggle key={a.code}
|
||||||
|
icon={a.icon}
|
||||||
|
active={(raum.align || 'mid') === a.code}
|
||||||
onClick={() => onUpdate({ align: a.code })}
|
onClick={() => onUpdate({ align: a.code })}
|
||||||
className={(raum.align || 'mid') === a.code ? 'btn-contained' : 'btn-outlined'}
|
title={a.label} />
|
||||||
style={{ flex: 1, padding: '3px 4px', fontSize: 10,
|
|
||||||
display: 'flex', alignItems: 'center',
|
|
||||||
justifyContent: 'center', gap: 3 }}
|
|
||||||
title={a.label}>
|
|
||||||
<Icon name={a.icon} size={12} />
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 60 }}>Texthöhe</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Texthöhe</span>
|
||||||
<input type="text" value={txtH}
|
<input type="text" value={txtH}
|
||||||
onChange={(e) => setTxtH(e.target.value)}
|
onChange={(e) => setTxtH(e.target.value)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@@ -912,7 +858,7 @@ function AussparungProperties({ aussp, onDelete }) {
|
|||||||
display: 'flex', flexDirection: 'column', gap: 8,
|
display: 'flex', flexDirection: 'column', gap: 8,
|
||||||
padding: 10, marginBottom: 8,
|
padding: 10, marginBottom: 8,
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
border: '1px solid var(--accent)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 'var(--r-lg)',
|
borderRadius: 'var(--r-lg)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
@@ -964,7 +910,7 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
|
|||||||
display: 'flex', flexDirection: 'column', gap: 8,
|
display: 'flex', flexDirection: 'column', gap: 8,
|
||||||
padding: 10, marginBottom: 8,
|
padding: 10, marginBottom: 8,
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
border: '1px solid var(--accent)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 'var(--r-lg)',
|
borderRadius: 'var(--r-lg)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
@@ -978,7 +924,7 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Geschoss</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Geschoss</span>
|
||||||
<select value={wall.geschoss}
|
<select value={wall.geschoss}
|
||||||
onChange={(e) => onUpdate({ geschoss: e.target.value })}
|
onChange={(e) => onUpdate({ geschoss: e.target.value })}
|
||||||
style={{ flex: 1, fontSize: 11 }}>
|
style={{ flex: 1, fontSize: 11 }}>
|
||||||
@@ -987,25 +933,20 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Aufbau</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Aufbau</span>
|
||||||
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
|
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
|
||||||
<button
|
<BarToggle label="Solid" active={!wall.layered}
|
||||||
onClick={() => onUpdate({ layered: false })}
|
onClick={() => onUpdate({ layered: false })}
|
||||||
className={!wall.layered ? 'btn-contained' : 'btn-outlined'}
|
title="Eine homogene Wand-Schicht (Standard)" />
|
||||||
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
|
<BarToggle label="Mehrschichtig" active={wall.layered}
|
||||||
title="Eine homogene Wand-Schicht (Standard)">Solid</button>
|
|
||||||
<button
|
|
||||||
onClick={() => onUpdate({ layered: true })}
|
onClick={() => onUpdate({ layered: true })}
|
||||||
className={wall.layered ? 'btn-contained' : 'btn-outlined'}
|
title="Mehrere Schichten mit individuellen Dicken und Farben" />
|
||||||
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
|
|
||||||
title="Mehrere Schichten mit individuellen Dicken und Farben"
|
|
||||||
>Mehrschichtig</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!wall.layered && (
|
{!wall.layered && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Dicke</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Dicke</span>
|
||||||
<input type="text" value={dicke}
|
<input type="text" value={dicke}
|
||||||
onChange={(e) => setDicke(e.target.value)}
|
onChange={(e) => setDicke(e.target.value)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@@ -1025,7 +966,7 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Referenz</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Referenz</span>
|
||||||
<ReferenzSelector value={wall.referenz || 'mid'}
|
<ReferenzSelector value={wall.referenz || 'mid'}
|
||||||
onChange={(v) => onUpdate({ referenz: v })} />
|
onChange={(v) => onUpdate({ referenz: v })} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1090,11 +1031,10 @@ function LayersEditor({ layers, onChange, materials }) {
|
|||||||
onChange={(patch) => updateAt(i, patch)}
|
onChange={(patch) => updateAt(i, patch)}
|
||||||
onRemove={() => removeAt(i)} />
|
onRemove={() => removeAt(i)} />
|
||||||
))}
|
))}
|
||||||
<button onClick={addLayer} className="btn-outlined"
|
<div style={{ marginTop: 2 }}>
|
||||||
style={{ padding: '3px 6px', fontSize: 10, marginTop: 2 }}
|
<BarToggle label="+ Schicht" onClick={addLayer}
|
||||||
title="Neue Schicht unten anfügen">
|
title="Neue Schicht unten anfügen" />
|
||||||
+ Schicht
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1183,7 +1123,7 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
|
|||||||
display: 'flex', flexDirection: 'column', gap: 8,
|
display: 'flex', flexDirection: 'column', gap: 8,
|
||||||
padding: 10, marginBottom: 8,
|
padding: 10, marginBottom: 8,
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
border: '1px solid var(--accent)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 'var(--r-lg)',
|
borderRadius: 'var(--r-lg)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
@@ -1197,7 +1137,7 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Geschoss</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Geschoss</span>
|
||||||
<select value={decke.geschoss}
|
<select value={decke.geschoss}
|
||||||
onChange={(e) => onUpdate({ geschoss: e.target.value })}
|
onChange={(e) => onUpdate({ geschoss: e.target.value })}
|
||||||
style={{ flex: 1, fontSize: 11 }}>
|
style={{ flex: 1, fontSize: 11 }}>
|
||||||
@@ -1206,7 +1146,7 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Dicke</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Dicke</span>
|
||||||
<input type="text" value={dicke}
|
<input type="text" value={dicke}
|
||||||
onChange={(e) => setDicke(e.target.value)}
|
onChange={(e) => setDicke(e.target.value)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@@ -1259,7 +1199,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
|
|||||||
display: 'flex', flexDirection: 'column', gap: 8,
|
display: 'flex', flexDirection: 'column', gap: 8,
|
||||||
padding: 10, marginBottom: 8,
|
padding: 10, marginBottom: 8,
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
border: '1px solid var(--accent)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 'var(--r-lg)',
|
borderRadius: 'var(--r-lg)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
@@ -1274,7 +1214,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
|
|||||||
|
|
||||||
{/* Dach-Typ */}
|
{/* Dach-Typ */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Typ</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Typ</span>
|
||||||
<select value={dachTyp}
|
<select value={dachTyp}
|
||||||
onChange={(e) => onUpdate({ dachTyp: e.target.value })}
|
onChange={(e) => onUpdate({ dachTyp: e.target.value })}
|
||||||
style={{ flex: 1, fontSize: 11 }}>
|
style={{ flex: 1, fontSize: 11 }}>
|
||||||
@@ -1286,7 +1226,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Geschoss</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Geschoss</span>
|
||||||
<select value={dach.geschoss}
|
<select value={dach.geschoss}
|
||||||
onChange={(e) => onUpdate({ geschoss: e.target.value })}
|
onChange={(e) => onUpdate({ geschoss: e.target.value })}
|
||||||
style={{ flex: 1, fontSize: 11 }}>
|
style={{ flex: 1, fontSize: 11 }}>
|
||||||
@@ -1295,7 +1235,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Dicke</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Dicke</span>
|
||||||
<input type="text" value={dicke}
|
<input type="text" value={dicke}
|
||||||
onChange={(e) => setDicke(e.target.value)}
|
onChange={(e) => setDicke(e.target.value)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@@ -1308,7 +1248,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Neigung</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Neigung</span>
|
||||||
<input type="text" value={neigung}
|
<input type="text" value={neigung}
|
||||||
onChange={(e) => setNeigung(e.target.value)}
|
onChange={(e) => setNeigung(e.target.value)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@@ -1323,7 +1263,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
|
|||||||
|
|
||||||
{dachTyp === 'pult' && (
|
{dachTyp === 'pult' && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Index der Traufkante (0 = Kante zwischen 1. und 2. Punkt)">
|
title="Index der Traufkante (0 = Kante zwischen 1. und 2. Punkt)">
|
||||||
Traufe
|
Traufe
|
||||||
</span>
|
</span>
|
||||||
@@ -1344,7 +1284,7 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
|
|||||||
{dachTyp === 'mansarde' && (
|
{dachTyp === 'mansarde' && (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Variante</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Variante</span>
|
||||||
<select value={dach.dachVariante || 'walm'}
|
<select value={dach.dachVariante || 'walm'}
|
||||||
onChange={(e) => onUpdate({ dachVariante: e.target.value })}
|
onChange={(e) => onUpdate({ dachVariante: e.target.value })}
|
||||||
style={{ flex: 1, fontSize: 11 }}>
|
style={{ flex: 1, fontSize: 11 }}>
|
||||||
@@ -1379,7 +1319,7 @@ function MansardeFields({ dach, onUpdate }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Untere (steile) Neigung — bis zum Knick">
|
title="Untere (steile) Neigung — bis zum Knick">
|
||||||
Steil
|
Steil
|
||||||
</span>
|
</span>
|
||||||
@@ -1395,7 +1335,7 @@ function MansardeFields({ dach, onUpdate }) {
|
|||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>°</span>
|
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>°</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Höhe über Traufe wo der Knick sitzt">
|
title="Höhe über Traufe wo der Knick sitzt">
|
||||||
Knick H
|
Knick H
|
||||||
</span>
|
</span>
|
||||||
@@ -1577,7 +1517,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
|
|||||||
display: 'flex', flexDirection: 'column', gap: 8,
|
display: 'flex', flexDirection: 'column', gap: 8,
|
||||||
padding: 10, marginBottom: 8,
|
padding: 10, marginBottom: 8,
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
border: '1px solid var(--accent)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 'var(--r-lg)',
|
borderRadius: 'var(--r-lg)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
@@ -1591,7 +1531,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Start</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Start</span>
|
||||||
<select value={treppe.geschoss}
|
<select value={treppe.geschoss}
|
||||||
onChange={(e) => onUpdate({ geschoss: e.target.value })}
|
onChange={(e) => onUpdate({ geschoss: e.target.value })}
|
||||||
style={{ flex: 1, fontSize: 11 }}>
|
style={{ flex: 1, fontSize: 11 }}>
|
||||||
@@ -1600,7 +1540,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Ziel</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Ziel</span>
|
||||||
<select
|
<select
|
||||||
value={hasHOver ? '__custom__' : (treppe.geschossEnd || '')}
|
value={hasHOver ? '__custom__' : (treppe.geschossEnd || '')}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -1621,7 +1561,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Breite</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
|
||||||
<input type="text" value={breite}
|
<input type="text" value={breite}
|
||||||
onChange={(e) => setBreite(e.target.value)}
|
onChange={(e) => setBreite(e.target.value)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@@ -1635,7 +1575,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Stufen</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Stufen</span>
|
||||||
<input type="text" value={nStufen}
|
<input type="text" value={nStufen}
|
||||||
onChange={(e) => setNStufen(e.target.value)}
|
onChange={(e) => setNStufen(e.target.value)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@@ -1649,15 +1589,13 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Lage</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Lage</span>
|
||||||
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
|
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
|
||||||
{REF_OPTIONS.map(o => (
|
{REF_OPTIONS.map(o => (
|
||||||
<button key={o.code}
|
<BarToggle key={o.code}
|
||||||
onClick={() => onUpdate({ treppeReferenz: o.code })}
|
label={o.label}
|
||||||
className={ref === o.code ? 'btn-contained' : 'btn-outlined'}
|
active={ref === o.code}
|
||||||
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}>
|
onClick={() => onUpdate({ treppeReferenz: o.code })} />
|
||||||
{o.label}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1666,19 +1604,17 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
|
|||||||
|
|
||||||
{/* Unterseite-Modus */}
|
{/* Unterseite-Modus */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Form der Treppen-Unterseite">
|
title="Form der Treppen-Unterseite">
|
||||||
Unten
|
Unten
|
||||||
</span>
|
</span>
|
||||||
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
|
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
|
||||||
{MODUS_OPTIONS.map(o => (
|
{MODUS_OPTIONS.map(o => (
|
||||||
<button key={o.code}
|
<BarToggle key={o.code}
|
||||||
|
label={o.label}
|
||||||
|
active={modus === o.code}
|
||||||
onClick={() => onUpdate({ treppeModus: o.code })}
|
onClick={() => onUpdate({ treppeModus: o.code })}
|
||||||
className={modus === o.code ? 'btn-contained' : 'btn-outlined'}
|
title={o.hint} />
|
||||||
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
|
|
||||||
title={o.hint}>
|
|
||||||
{o.label}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1686,7 +1622,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
|
|||||||
{/* Lauf-Plattendicke (nur fuer flach + plattenrand relevant) */}
|
{/* Lauf-Plattendicke (nur fuer flach + plattenrand relevant) */}
|
||||||
{modus !== 'massiv' && (
|
{modus !== 'massiv' && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Dicke der Lauf-Platte (Materialdicke unter den Stufen)">
|
title="Dicke der Lauf-Platte (Materialdicke unter den Stufen)">
|
||||||
Platte
|
Platte
|
||||||
</span>
|
</span>
|
||||||
@@ -1782,7 +1718,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
display: 'flex', flexDirection: 'column', gap: 8,
|
display: 'flex', flexDirection: 'column', gap: 8,
|
||||||
padding: 10, marginBottom: 8,
|
padding: 10, marginBottom: 8,
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
border: '1px solid var(--accent)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 'var(--r-lg)',
|
borderRadius: 'var(--r-lg)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
@@ -1796,7 +1732,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Breite</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
|
||||||
<input type="text" value={breite}
|
<input type="text" value={breite}
|
||||||
onChange={(e) => setBreite(e.target.value)}
|
onChange={(e) => setBreite(e.target.value)}
|
||||||
onBlur={() => commit('breite', breite, setBreite, oeff.breite)}
|
onBlur={() => commit('breite', breite, setBreite, oeff.breite)}
|
||||||
@@ -1806,7 +1742,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>Höhe</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Höhe</span>
|
||||||
<input type="text" value={hoehe}
|
<input type="text" value={hoehe}
|
||||||
onChange={(e) => setHoehe(e.target.value)}
|
onChange={(e) => setHoehe(e.target.value)}
|
||||||
onBlur={() => commit('hoehe', hoehe, setHoehe, oeff.hoehe)}
|
onBlur={() => commit('hoehe', hoehe, setHoehe, oeff.hoehe)}
|
||||||
@@ -1816,7 +1752,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title={isFenster
|
title={isFenster
|
||||||
? 'Brüstungshöhe über UK der Wand'
|
? 'Brüstungshöhe über UK der Wand'
|
||||||
: 'Türschwelle / Höhe über UK der Wand'}>
|
: 'Türschwelle / Höhe über UK der Wand'}>
|
||||||
@@ -1836,19 +1772,17 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
|
|
||||||
{/* Referenz-Lage: wo sitzt der Klick-Punkt in der Oeffnung */}
|
{/* Referenz-Lage: wo sitzt der Klick-Punkt in der Oeffnung */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Lage des Klick-Punkts in der Öffnung">
|
title="Lage des Klick-Punkts in der Öffnung">
|
||||||
Ref
|
Ref
|
||||||
</span>
|
</span>
|
||||||
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
|
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
|
||||||
{OEFF_REFERENZ_OPTIONS.map(o => (
|
{OEFF_REFERENZ_OPTIONS.map(o => (
|
||||||
<button key={o.code}
|
<BarToggle key={o.code}
|
||||||
|
label={o.label}
|
||||||
|
active={(oeff.oeffReferenz || 'mid') === o.code}
|
||||||
onClick={() => onUpdate({ oeffReferenz: o.code })}
|
onClick={() => onUpdate({ oeffReferenz: o.code })}
|
||||||
className={(oeff.oeffReferenz || 'mid') === o.code ? 'btn-contained' : 'btn-outlined'}
|
title={o.hint} />
|
||||||
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
|
|
||||||
title={o.hint}>
|
|
||||||
{o.label}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1857,7 +1791,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
|
|
||||||
{/* Rahmen-Profil (Breite × Tiefe) — beide Felder gleich breit */}
|
{/* Rahmen-Profil (Breite × Tiefe) — beide Felder gleich breit */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Rahmen-Profil: Breite (in Wandflaeche) × Tiefe (entlang Wandnormale)">
|
title="Rahmen-Profil: Breite (in Wandflaeche) × Tiefe (entlang Wandnormale)">
|
||||||
Rahmen
|
Rahmen
|
||||||
</span>
|
</span>
|
||||||
@@ -1879,19 +1813,17 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
|
|
||||||
{/* Rahmen-Lage im Wandquerschnitt */}
|
{/* Rahmen-Lage im Wandquerschnitt */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Lage des Rahmens im Wandquerschnitt">
|
title="Lage des Rahmens im Wandquerschnitt">
|
||||||
Lage
|
Lage
|
||||||
</span>
|
</span>
|
||||||
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
|
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
|
||||||
{RAHMEN_POS_OPTIONS.map(o => (
|
{RAHMEN_POS_OPTIONS.map(o => (
|
||||||
<button key={o.code}
|
<BarToggle key={o.code}
|
||||||
|
label={o.label}
|
||||||
|
active={rahmenPos === o.code}
|
||||||
onClick={() => onUpdate({ rahmenPos: o.code })}
|
onClick={() => onUpdate({ rahmenPos: o.code })}
|
||||||
className={rahmenPos === o.code ? 'btn-contained' : 'btn-outlined'}
|
title={o.hint} />
|
||||||
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
|
|
||||||
title={o.hint}>
|
|
||||||
{o.label}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1899,18 +1831,16 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
{/* Fluegel-Anzahl — nur fuer Fenster (Tueren haben ein einzelnes Tuerblatt) */}
|
{/* Fluegel-Anzahl — nur fuer Fenster (Tueren haben ein einzelnes Tuerblatt) */}
|
||||||
{isFenster && (
|
{isFenster && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Anzahl Flügel (vertikale Unterteilung)">
|
title="Anzahl Flügel (vertikale Unterteilung)">
|
||||||
Flügel
|
Flügel
|
||||||
</span>
|
</span>
|
||||||
<div style={{ flex: 1, display: 'flex', gap: 2 }}>
|
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
|
||||||
{[1, 2, 3, 4].map(n => (
|
{[1, 2, 3, 4].map(n => (
|
||||||
<button key={n}
|
<BarToggle key={n}
|
||||||
onClick={() => onUpdate({ fluegel: n })}
|
label={String(n)}
|
||||||
className={fluegel === n ? 'btn-contained' : 'btn-outlined'}
|
active={fluegel === n}
|
||||||
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}>
|
onClick={() => onUpdate({ fluegel: n })} />
|
||||||
{n}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1920,7 +1850,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
{isFenster && (
|
{isFenster && (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Aussensims — Platte unter Öffnung, ragt aussen heraus">
|
title="Aussensims — Platte unter Öffnung, ragt aussen heraus">
|
||||||
Sims a.
|
Sims a.
|
||||||
</span>
|
</span>
|
||||||
@@ -1932,7 +1862,7 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
|
||||||
title="Innensims — Platte unter Öffnung, ragt innen heraus">
|
title="Innensims — Platte unter Öffnung, ragt innen heraus">
|
||||||
Sims i.
|
Sims i.
|
||||||
</span>
|
</span>
|
||||||
@@ -1948,13 +1878,11 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
|
|
||||||
{/* Glas-Toggle: bei Tueren ersetzt Glas das Tuerblatt (verglaste Tuer) */}
|
{/* Glas-Toggle: bei Tueren ersetzt Glas das Tuerblatt (verglaste Tuer) */}
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
<button
|
<BarToggle
|
||||||
|
label={(isFenster ? 'Glas' : 'Verglast') + (oeff.glas ? ' ✓' : '')}
|
||||||
|
active={!!oeff.glas}
|
||||||
onClick={() => onUpdate({ glas: !oeff.glas })}
|
onClick={() => onUpdate({ glas: !oeff.glas })}
|
||||||
className={oeff.glas ? 'btn-contained' : 'btn-outlined'}
|
title={isFenster ? 'Glasscheibe sichtbar' : 'Verglaste Tür (statt Türblatt)'} />
|
||||||
style={{ flex: 1, padding: '3px 6px', fontSize: 10 }}
|
|
||||||
title={isFenster ? 'Glasscheibe sichtbar' : 'Verglaste Tür (statt Türblatt)'}>
|
|
||||||
{isFenster ? 'Glas' : 'Verglast'} {oeff.glas ? '✓' : ''}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -1965,13 +1893,10 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
|
|||||||
function AutoOverrideField({ label, auto, autoValue, rawValue, onChangeRaw, onToggle, onCommit }) {
|
function AutoOverrideField({ label, auto, autoValue, rawValue, onChangeRaw, onToggle, onCommit }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', width: 50 }}>{label}</span>
|
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>{label}</span>
|
||||||
<button onClick={onToggle}
|
<BarToggle label={auto ? 'Auto' : 'Custom'} active={auto}
|
||||||
className={auto ? 'btn-contained' : 'btn-outlined'}
|
onClick={onToggle}
|
||||||
style={{ padding: '3px 8px', fontSize: 10 }}
|
title={auto ? 'Folgt Geschoss' : 'Eigener Wert'} />
|
||||||
title={auto ? 'Folgt Geschoss' : 'Eigener Wert'}>
|
|
||||||
{auto ? 'Auto' : 'Custom'}
|
|
||||||
</button>
|
|
||||||
<input type="text"
|
<input type="text"
|
||||||
value={auto ? fmtNum(autoValue) : rawValue}
|
value={auto ? fmtNum(autoValue) : rawValue}
|
||||||
disabled={auto}
|
disabled={auto}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import Icon from './components/Icon'
|
||||||
|
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||||
|
import ElementeApp, { PropertiesView } from './ElementeApp.jsx'
|
||||||
|
|
||||||
|
// Satellite-Window — zeigt nur die Properties des aktuell selektierten
|
||||||
|
// Elements in einem groesseren Fenster. Daten kommen vom ElementeBridge
|
||||||
|
// (via STATE-Forward in elemente.py).
|
||||||
|
export default function ElementePropertiesApp() {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
elements: [], geschosse: [], selection: null,
|
||||||
|
materials: [], hatchPatterns: [],
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
onMessage('STATE', (s) => setState(prev => ({ ...prev, ...s })))
|
||||||
|
notifyReady()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const elements = state.elements || []
|
||||||
|
const selected = elements.find(el => el.id === state.selection)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
height: '100vh', overflow: 'hidden',
|
||||||
|
background: 'var(--bg-base)', color: 'var(--text-primary)',
|
||||||
|
fontFamily: 'var(--font)', fontSize: 11,
|
||||||
|
}}>
|
||||||
|
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 12 }}>
|
||||||
|
{selected ? (
|
||||||
|
<PropertiesView
|
||||||
|
selected={selected}
|
||||||
|
geschosse={state.geschosse || []}
|
||||||
|
materials={state.materials || []}
|
||||||
|
hatchPatterns={state.hatchPatterns}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{
|
||||||
|
padding: 60, textAlign: 'center',
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Icon name="touch_app" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
||||||
|
<div>Kein Element selektiert.</div>
|
||||||
|
<div style={{ fontSize: 10 }}>Im Viewport ein Element wählen.</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
height: '100vh', overflow: 'hidden',
|
||||||
|
background: 'var(--bg-base)', color: 'var(--text-primary)',
|
||||||
|
fontFamily: 'var(--font)', fontSize: 11,
|
||||||
|
}}>
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', flexDirection: 'column', gap: 6,
|
||||||
|
padding: '8px 10px',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<input type="text" placeholder="Suchen…"
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => 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',
|
||||||
|
}} />
|
||||||
|
<BarButton icon="unfold_more" onClick={expandAll} title="Alle aufklappen" />
|
||||||
|
<BarButton icon="unfold_less" onClick={collapseAll} title="Alle einklappen" />
|
||||||
|
</div>
|
||||||
|
{/* Kind-Filter Chips */}
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
|
||||||
|
<BarToggle label={`Alle ${totalCount > 0 ? '· ' + totalCount : ''}`}
|
||||||
|
active={!filterKind}
|
||||||
|
onClick={() => setFilterKind('')} />
|
||||||
|
{KIND_ORDER.filter(k => kindCounts[k]).map(k => {
|
||||||
|
const meta = KIND_META[k]
|
||||||
|
return (
|
||||||
|
<BarToggle key={k}
|
||||||
|
icon={meta.icon}
|
||||||
|
label={`${meta.label} ${kindCounts[k]}`}
|
||||||
|
active={filterKind === k}
|
||||||
|
onClick={() => setFilterKind(filterKind === k ? '' : k)} />
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tree */}
|
||||||
|
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||||
|
{totalCount === 0 ? (
|
||||||
|
<div style={{
|
||||||
|
padding: 40, textAlign: 'center',
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Icon name="inventory_2" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
||||||
|
<div>Keine Elemente im Projekt.</div>
|
||||||
|
<div style={{ fontSize: 10 }}>Wände, Decken, Türen etc. via Elemente-Panel anlegen.</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
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 (
|
||||||
|
<div key={g.id}>
|
||||||
|
<div onClick={() => 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',
|
||||||
|
}}>
|
||||||
|
<Icon name={gOpen ? 'expand_more' : 'chevron_right'}
|
||||||
|
size={14} style={{ color: 'var(--text-muted)' }} />
|
||||||
|
<span style={{
|
||||||
|
fontSize: 11, fontWeight: 600,
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
letterSpacing: '0.04em', textTransform: 'uppercase',
|
||||||
|
}}>{g.name}</span>
|
||||||
|
{g.okff != null && (
|
||||||
|
<span style={{
|
||||||
|
fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
fontFamily: 'DM Mono, monospace',
|
||||||
|
}}>+{g.okff.toFixed(2)} m</span>
|
||||||
|
)}
|
||||||
|
<span style={{ flex: 1 }} />
|
||||||
|
<span style={{
|
||||||
|
fontSize: 10, color: 'var(--text-muted)',
|
||||||
|
fontFamily: 'DM Mono, monospace',
|
||||||
|
}}>{total}</span>
|
||||||
|
</div>
|
||||||
|
{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 (
|
||||||
|
<div key={kKey}>
|
||||||
|
<div onClick={() => 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',
|
||||||
|
}}>
|
||||||
|
<Icon name={kOpen ? 'expand_more' : 'chevron_right'}
|
||||||
|
size={11} style={{ color: 'var(--text-muted)' }} />
|
||||||
|
<Icon name={meta.icon} size={12} style={{ color: meta.color }} />
|
||||||
|
<span style={{
|
||||||
|
fontSize: 10, color: 'var(--text-muted)',
|
||||||
|
letterSpacing: '0.06em', textTransform: 'uppercase',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}>{meta.label}</span>
|
||||||
|
<span style={{ flex: 1 }} />
|
||||||
|
<span style={{
|
||||||
|
fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
fontFamily: 'DM Mono, monospace',
|
||||||
|
}}>{arr.length}</span>
|
||||||
|
</div>
|
||||||
|
{kOpen && arr.map((it, idx) => (
|
||||||
|
<div key={it.id}
|
||||||
|
onClick={(ev) => 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'
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
fontFamily: 'DM Mono, monospace',
|
||||||
|
minWidth: 24, textAlign: 'right',
|
||||||
|
}}>{String(idx + 1).padStart(2, '0')}</span>
|
||||||
|
<span style={{
|
||||||
|
flex: 1, minWidth: 0,
|
||||||
|
fontSize: 11,
|
||||||
|
color: it.selected ? 'var(--accent-light)' : 'var(--text-label)',
|
||||||
|
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||||
|
}}>{it.name || meta.label}</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 10, color: 'var(--text-muted)',
|
||||||
|
fontFamily: 'DM Mono, monospace',
|
||||||
|
}}>{it.info}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
padding: '4px 10px',
|
||||||
|
background: 'var(--bg-section)',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
fontSize: 10, color: 'var(--text-muted)',
|
||||||
|
}}>
|
||||||
|
<span>{filteredCount} {filteredCount !== totalCount && `von ${totalCount}`} Elemente</span>
|
||||||
|
<span style={{ flex: 1 }} />
|
||||||
|
<span style={{ fontStyle: 'italic' }}>
|
||||||
|
Klick = selektieren · Shift+Klick = zoomen
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+109
-135
@@ -1,8 +1,10 @@
|
|||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import Icon from './components/Icon'
|
import Icon from './components/Icon'
|
||||||
|
import { BarCombo, BarToggle, BarButton } from './components/BarControls'
|
||||||
import {
|
import {
|
||||||
onMessage, notifyReady,
|
onMessage, notifyReady,
|
||||||
requestSelection, setColorSource, setLwSource, setLinetypeSource, setLinetypeScale, setFill,
|
setColorSource, setLwSource, setLinetypeSource, setLinetypeScale, setFill,
|
||||||
|
setSectionStyle,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const LW_PRESETS = [0.02, 0.10, 0.13, 0.18, 0.25, 0.35, 0.50, 0.70, 1.00]
|
const LW_PRESETS = [0.02, 0.10, 0.13, 0.18, 0.25, 0.35, 0.50, 0.70, 1.00]
|
||||||
@@ -20,36 +22,20 @@ function SectionHead({ title }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Source-Dropdown im Vectorworks-Stil: Link-Icon + Label + Caret */
|
/** Source-Dropdown: BarCombo mit Link-Icon (active wenn Nach Ebene) */
|
||||||
function SourceSelect({ source, onChange, overrideLabel = 'Eigene' }) {
|
function SourceSelect({ source, onChange, overrideLabel = 'Eigene' }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative', display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<Icon
|
<BarCombo
|
||||||
name={source === 'layer' ? 'link' : 'link_off'}
|
icon={source === 'layer' ? 'link' : 'link_off'}
|
||||||
size={14}
|
iconActive={source === 'layer'}
|
||||||
style={{
|
|
||||||
position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
|
|
||||||
color: source === 'layer' ? 'var(--text-primary)' : 'var(--text-muted)',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={source}
|
value={source}
|
||||||
onChange={(ev) => onChange(ev.target.value)}
|
onChange={onChange}
|
||||||
style={{
|
title="Farb-Quelle"
|
||||||
paddingLeft: 30, flex: 1,
|
|
||||||
fontFamily: 'var(--font)',
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: 11,
|
|
||||||
textTransform: 'none',
|
|
||||||
/* borderRadius/background uebernehmen wir von der globalen <select>-
|
|
||||||
Pille (index.css) damit's optisch konsistent mit dem Fill-Dropdown
|
|
||||||
wirkt. Nur paddingLeft ueberschreiben wegen Link-Icon. */
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<option value="layer">Nach Ebene</option>
|
<option value="layer">Nach Ebene</option>
|
||||||
<option value="object">{overrideLabel}</option>
|
<option value="object">{overrideLabel}</option>
|
||||||
</select>
|
</BarCombo>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -62,7 +48,7 @@ function ColorBar({ color, onChange, height = 22 }) {
|
|||||||
style={{
|
style={{
|
||||||
width: '100%', height,
|
width: '100%', height,
|
||||||
background: color,
|
background: color,
|
||||||
borderRadius: 'var(--r)',
|
borderRadius: 999,
|
||||||
border: '1px solid var(--border)',
|
border: '1px solid var(--border)',
|
||||||
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.08)',
|
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.08)',
|
||||||
}}
|
}}
|
||||||
@@ -99,7 +85,7 @@ function ColorBar({ color, onChange, height = 22 }) {
|
|||||||
style={{
|
style={{
|
||||||
position: 'relative', display: 'block',
|
position: 'relative', display: 'block',
|
||||||
width: '100%', height,
|
width: '100%', height,
|
||||||
borderRadius: 'var(--r)',
|
borderRadius: 999,
|
||||||
border: '1px solid var(--border)',
|
border: '1px solid var(--border)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -214,14 +200,12 @@ function PenLw({ sel }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<button
|
<BarToggle
|
||||||
className="btn-icon-sm"
|
icon={source === 'layer' ? 'link' : 'link_off'}
|
||||||
|
active={source === 'layer'}
|
||||||
onClick={() => setLwSource(source === 'object' ? 'layer' : 'object', source === 'object' ? null : effective)}
|
onClick={() => setLwSource(source === 'object' ? 'layer' : 'object', source === 'object' ? null : effective)}
|
||||||
title={source === 'object' ? 'Nach Ebene' : 'Übersteuern'}
|
title={source === 'object' ? 'Nach Ebene' : 'Übersteuern'}
|
||||||
style={{ color: source === 'layer' ? 'var(--text-primary)' : 'var(--text-muted)' }}
|
/>
|
||||||
>
|
|
||||||
<Icon name={source === 'layer' ? 'link' : 'link_off'} size={14} />
|
|
||||||
</button>
|
|
||||||
<LwPreview lw={effective} />
|
<LwPreview lw={effective} />
|
||||||
<NumInput
|
<NumInput
|
||||||
value={effective}
|
value={effective}
|
||||||
@@ -230,8 +214,8 @@ function PenLw({ sel }) {
|
|||||||
width={52}
|
width={52}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<button className="btn-step" onClick={() => step(+1)}><Icon name="arrow_drop_up" size={12}/></button>
|
<button className="btn-step" onClick={() => step(+1)}><Icon name="arrow_drop_up" size={11}/></button>
|
||||||
<button className="btn-step" onClick={() => step(-1)}><Icon name="arrow_drop_down" size={12}/></button>
|
<button className="btn-step" onClick={() => step(-1)}><Icon name="arrow_drop_down" size={11}/></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -259,31 +243,18 @@ function PenLinetype({ sel }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ position: 'relative', display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<Icon
|
<BarCombo
|
||||||
name={source === 'layer' ? 'link' : 'link_off'}
|
icon={source === 'layer' ? 'link' : 'link_off'}
|
||||||
size={14}
|
iconActive={source === 'layer'}
|
||||||
style={{
|
|
||||||
position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
|
|
||||||
color: source === 'layer' ? 'var(--text-primary)' : 'var(--text-muted)',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={ltCurrent}
|
value={ltCurrent}
|
||||||
onChange={(ev) => ltChange(ev.target.value)}
|
onChange={ltChange}
|
||||||
style={{
|
title="Linientyp-Quelle"
|
||||||
paddingLeft: 30, flex: 1,
|
|
||||||
fontFamily: 'var(--font)',
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: 11,
|
|
||||||
textTransform: 'none',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<option value="__layer__">Nach Ebene</option>
|
<option value="__layer__">Nach Ebene</option>
|
||||||
{!hasOption && effective && <option value={effective}>{effective}</option>}
|
{!hasOption && effective && <option value={effective}>{effective}</option>}
|
||||||
{options.map(n => <option key={n} value={n}>{n}</option>)}
|
{options.map(n => <option key={n} value={n}>{n}</option>)}
|
||||||
</select>
|
</BarCombo>
|
||||||
</div>
|
</div>
|
||||||
{!isSolid && (
|
{!isSolid && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
@@ -317,61 +288,34 @@ function PenBlock({ sel }) {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function FillBlock({ sel }) {
|
// Gemeinsamer Hatch-Editor — wiederverwendet fuer Fill (2D-Curves) und
|
||||||
if (sel.canFill !== true) {
|
// Section Style (3D-Solids). Beide haben die gleichen Einstellungs-
|
||||||
return (
|
// moeglichkeiten: Pattern/Color/Scale/Rotation + "Nach Ebene"-Switch.
|
||||||
<div style={{ padding: '0 14px 12px', fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
// `setter` bekommt (enabled, source, color, pattern, scale, rotation).
|
||||||
Keine geschlossenen 2D-Kurven in der Auswahl.
|
function HatchEditor({ sel, enabled, source, color, pattern, scale, rotation,
|
||||||
</div>
|
layerHint, setter, patternList }) {
|
||||||
)
|
const objectPat = enabled ? (pattern || 'Solid') : 'None'
|
||||||
}
|
|
||||||
const enabled = sel.fillEnabled === true
|
|
||||||
const source = sel.fillSource || 'layer'
|
|
||||||
const color = sel.fillColor || sel.layerColor || '#cccccc'
|
|
||||||
const objectPat = enabled ? (sel.fillPattern || 'Solid') : 'None'
|
|
||||||
const scale = sel.fillScale ?? 1.0
|
|
||||||
const rotation = sel.fillRotation ?? 0.0
|
|
||||||
const patternList = sel.hatchPatterns || ['Solid']
|
|
||||||
|
|
||||||
// Special-Value im kombinierten Dropdown:
|
|
||||||
// "__layer__" = Nach Ebene -> Pattern/Scale/Rot/Color aus Ebenen-Einstellungen
|
|
||||||
// "None" = keine Fuellung
|
|
||||||
// "Solid" = volle Flaeche (Eigene Quelle)
|
|
||||||
// sonst = Hatch-Pattern (Eigene Quelle)
|
|
||||||
// source=='layer' -> immer "Nach Ebene" zeigen, auch wenn (noch) keine Hatch
|
|
||||||
// existiert (Ebene hat halt aktuell kein Pattern -> wird gefuellt, sobald
|
|
||||||
// eines definiert wird).
|
|
||||||
const currentValue = source === 'layer'
|
const currentValue = source === 'layer'
|
||||||
? '__layer__'
|
? '__layer__'
|
||||||
: (!enabled ? 'None' : objectPat)
|
: (!enabled ? 'None' : objectPat)
|
||||||
|
|
||||||
const dropdownOptions = [
|
const dropdownOptions = [
|
||||||
{ value: '__layer__', label: 'Nach Ebene' },
|
{ value: '__layer__', label: 'Nach Ebene' },
|
||||||
{ value: 'None', label: 'None' },
|
{ value: 'None', label: 'None' },
|
||||||
{ value: 'Solid', label: 'Solid' },
|
{ value: 'Solid', label: 'Solid' },
|
||||||
...patternList
|
...(patternList || [])
|
||||||
.filter(p => p !== 'Solid' && p !== 'None')
|
.filter(p => p !== 'Solid' && p !== 'None')
|
||||||
.map(p => ({ value: p, label: p })),
|
.map(p => ({ value: p, label: p })),
|
||||||
]
|
]
|
||||||
// Falls aktueller Pattern-Name nicht in der Liste, anhaengen damit's nicht verloren geht
|
|
||||||
if (currentValue !== '__layer__' && currentValue !== 'None'
|
if (currentValue !== '__layer__' && currentValue !== 'None'
|
||||||
&& !dropdownOptions.some(o => o.value === currentValue)) {
|
&& !dropdownOptions.some(o => o.value === currentValue)) {
|
||||||
dropdownOptions.push({ value: currentValue, label: currentValue })
|
dropdownOptions.push({ value: currentValue, label: currentValue })
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyPattern = (newValue) => {
|
const applyPattern = (newValue) => {
|
||||||
if (newValue === '__layer__') {
|
if (newValue === '__layer__') setter(true, 'layer', null, null, null, null)
|
||||||
// Source -> layer, Python liest Pattern/Scale/Rot/Color aus Layer.SectionStyle
|
else if (newValue === 'None') setter(false, source, null, null, scale, rotation)
|
||||||
setFill(true, 'layer', null, null, null, null)
|
else setter(true, 'object', color, newValue, scale, rotation)
|
||||||
} else if (newValue === 'None') {
|
|
||||||
setFill(false, source, null, null, scale, rotation)
|
|
||||||
} else {
|
|
||||||
// Eigene Quelle mit gewaehltem Pattern
|
|
||||||
setFill(true, 'object', color, newValue, scale, rotation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Anpassungen wenn schon im "Eigene"-Modus (Scale/Rotation/Color/Source-Toggle)
|
const apply = (over) => setter(
|
||||||
const apply = (over) => setFill(
|
|
||||||
true,
|
true,
|
||||||
over.source ?? (source === 'layer' ? 'object' : source),
|
over.source ?? (source === 'layer' ? 'object' : source),
|
||||||
(over.source ?? source) === 'layer' ? null : (over.color ?? color),
|
(over.source ?? source) === 'layer' ? null : (over.color ?? color),
|
||||||
@@ -383,33 +327,18 @@ function FillBlock({ sel }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '0 14px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div style={{ padding: '0 14px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
{/* Pill mit Link-Icon im Dropdown (analog zur Pen-Source-Pill).
|
<div style={{ display: 'flex' }}>
|
||||||
Link-Icon wenn "Nach Ebene", sonst link_off. */}
|
<BarCombo
|
||||||
<div style={{ position: 'relative', display: 'flex' }}>
|
icon={isLayerSource ? 'link' : 'link_off'}
|
||||||
<Icon
|
iconActive={isLayerSource}
|
||||||
name={isLayerSource ? 'link' : 'link_off'}
|
|
||||||
size={14}
|
|
||||||
style={{
|
|
||||||
position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
|
|
||||||
color: isLayerSource ? 'var(--text-primary)' : 'var(--text-muted)',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
onChange={(ev) => applyPattern(ev.target.value)}
|
onChange={applyPattern}
|
||||||
style={{
|
title="Pattern-Quelle"
|
||||||
paddingLeft: 30, flex: 1,
|
|
||||||
fontFamily: 'var(--font)',
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: 11,
|
|
||||||
textTransform: 'none',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{dropdownOptions.map(o => (
|
{dropdownOptions.map(o => (
|
||||||
<option key={o.value} value={o.value}>{o.label}</option>
|
<option key={o.value} value={o.value}>{o.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</BarCombo>
|
||||||
</div>
|
</div>
|
||||||
{currentValue !== 'None' && (
|
{currentValue !== 'None' && (
|
||||||
<>
|
<>
|
||||||
@@ -436,7 +365,7 @@ function FillBlock({ sel }) {
|
|||||||
)}
|
)}
|
||||||
{isLayerSource && (
|
{isLayerSource && (
|
||||||
<div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
<div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
||||||
Pattern, Skalierung & Farbe folgen Layer-Section-Style
|
{layerHint}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -445,6 +374,46 @@ function FillBlock({ sel }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FillBlock({ sel }) {
|
||||||
|
if (sel.canFill !== true) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '0 14px 12px', fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
||||||
|
Keine geschlossenen 2D-Kurven in der Auswahl.
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const color = sel.fillColor || sel.layerColor || '#cccccc'
|
||||||
|
return <HatchEditor
|
||||||
|
sel={sel}
|
||||||
|
enabled={sel.fillEnabled === true}
|
||||||
|
source={sel.fillSource || 'layer'}
|
||||||
|
color={color}
|
||||||
|
pattern={sel.fillPattern || 'Solid'}
|
||||||
|
scale={sel.fillScale ?? 1.0}
|
||||||
|
rotation={sel.fillRotation ?? 0.0}
|
||||||
|
patternList={sel.hatchPatterns || ['Solid']}
|
||||||
|
layerHint="Pattern, Skalierung & Farbe folgen Layer-Section-Style"
|
||||||
|
setter={setFill}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionBlock({ sel }) {
|
||||||
|
const color = sel.sectionColor || sel.layerColor || '#cccccc'
|
||||||
|
return <HatchEditor
|
||||||
|
sel={sel}
|
||||||
|
enabled={sel.sectionEnabled === true}
|
||||||
|
source={sel.sectionSource || 'layer'}
|
||||||
|
color={color}
|
||||||
|
pattern={sel.sectionPattern || 'Solid'}
|
||||||
|
scale={sel.sectionScale ?? 1.0}
|
||||||
|
rotation={sel.sectionRotation ?? 0.0}
|
||||||
|
patternList={sel.hatchPatterns || ['Solid']}
|
||||||
|
layerHint="Pattern, Skalierung & Farbe folgen Layer-SectionStyle"
|
||||||
|
setter={setSectionStyle}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function EmptyState() {
|
function EmptyState() {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -472,6 +441,15 @@ export default function GestaltungApp() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const empty = sel.count === 0
|
const empty = sel.count === 0
|
||||||
|
// geometryKind kommt vom Backend — entscheidet welche Sections passend sind:
|
||||||
|
// curve → Fill + Pen (2D-Workflow: Hatch + Lineweight/Color)
|
||||||
|
// 3d → Section Style + Boundary (Solid: Schnitt-Hatch + Silhouetten)
|
||||||
|
// curveOpen → Pen (offene Kurve hat keine Fill)
|
||||||
|
// mixed → Pen only (gemischte Selektion: nur die gemeinsame Untermenge)
|
||||||
|
const kind = sel.geometryKind || 'curve'
|
||||||
|
const showFill = kind === 'curve'
|
||||||
|
const showSection = kind === '3d'
|
||||||
|
const penLabel = (kind === '3d') ? 'Boundary' : 'Pen'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -480,30 +458,26 @@ export default function GestaltungApp() {
|
|||||||
background: 'var(--bg-base)',
|
background: 'var(--bg-base)',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
|
||||||
display: 'flex', alignItems: 'center', gap: 8,
|
|
||||||
padding: '10px 14px',
|
|
||||||
borderBottom: '1px solid var(--border-light)',
|
|
||||||
}}>
|
|
||||||
<Icon name="tune" size={16} style={{ color: 'var(--text-muted)' }} />
|
|
||||||
<span style={{ flex: 1, fontWeight: 500, fontSize: 12, color: 'var(--text-primary)' }}>
|
|
||||||
Attribute
|
|
||||||
</span>
|
|
||||||
{sel.count > 0 && <span className="chip">{sel.count}</span>}
|
|
||||||
<button className="btn-icon-sm" onClick={() => requestSelection()} title="Aktualisieren">
|
|
||||||
<Icon name="refresh" size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||||
{empty ? <EmptyState /> : (
|
{empty ? <EmptyState /> : (
|
||||||
<>
|
<>
|
||||||
<SectionHead title="Fill" />
|
{showFill && (
|
||||||
<FillBlock sel={sel} />
|
<>
|
||||||
|
<SectionHead title="Fill" />
|
||||||
|
<FillBlock sel={sel} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<SectionHead title="Pen" />
|
<SectionHead title={penLabel} />
|
||||||
<PenBlock sel={sel} />
|
<PenBlock sel={sel} />
|
||||||
|
|
||||||
|
{showSection && (
|
||||||
|
<>
|
||||||
|
<SectionHead title="Section Style" />
|
||||||
|
<SectionBlock sel={sel} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<SectionHead title="Effects" />
|
<SectionHead title="Effects" />
|
||||||
<div style={{ padding: '0 14px 14px', fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
<div style={{ padding: '0 14px 14px', fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
||||||
Schatten / Transparenz folgen spaeter.
|
Schatten / Transparenz folgen spaeter.
|
||||||
|
|||||||
@@ -245,16 +245,16 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
|
|||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 4,
|
display: 'flex', alignItems: 'center', gap: 4,
|
||||||
padding: '1px 12px',
|
padding: '1px 8px',
|
||||||
paddingLeft: 12 + (depth || 0) * 12,
|
paddingLeft: 6 + (depth || 0) * 10,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
background: active ? 'var(--active-dim)'
|
background: active ? 'var(--active-dim)'
|
||||||
: (e.visible !== false) ? 'var(--bg-item)'
|
: (e.visible !== false) ? 'var(--bg-item)'
|
||||||
: 'var(--bg-panel)',
|
: 'var(--bg-panel)',
|
||||||
// Eckige Tabellen-Zeile mit Accent-Strip links fuer aktive Ebene
|
// Aktive Zeile als Pill — kein Margin/Shift damit Inhalt nicht
|
||||||
borderRadius: 0,
|
// springt, nur Bg-Form aendert sich.
|
||||||
borderLeft: '3px solid ' + (active ? 'var(--accent)' : 'transparent'),
|
borderRadius: active ? 999 : 0,
|
||||||
borderBottom: '1px solid var(--border-light)',
|
borderBottom: active ? '1px solid transparent' : '1px solid var(--border-light)',
|
||||||
opacity: (!active && e.visible === false && mode !== 'all') ? 0.45 : 1,
|
opacity: (!active && e.visible === false && mode !== 'all') ? 0.45 : 1,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
@@ -293,7 +293,7 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
|
|||||||
value={e.name}
|
value={e.name}
|
||||||
onCommit={onNameChange}
|
onCommit={onNameChange}
|
||||||
autoEditTrigger={autoEditName}
|
autoEditTrigger={autoEditName}
|
||||||
fontWeight={active ? 700 : 400}
|
fontWeight={500}
|
||||||
fontSize={11}
|
fontSize={11}
|
||||||
style={{
|
style={{
|
||||||
color: active ? 'var(--active-light)'
|
color: active ? 'var(--active-light)'
|
||||||
@@ -565,8 +565,8 @@ export default function EbenenManager({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 5,
|
display: 'flex', alignItems: 'center', gap: 4,
|
||||||
padding: '2px 14px',
|
padding: '2px 8px 2px 9px',
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
borderBottom: '1px solid var(--border)',
|
borderBottom: '1px solid var(--border)',
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Icon from './Icon'
|
import Icon from './Icon'
|
||||||
|
import { BarCombo } from './BarControls'
|
||||||
|
|
||||||
function Field({ label, hint, children }) {
|
function Field({ label, hint, children }) {
|
||||||
return (
|
return (
|
||||||
@@ -123,19 +124,19 @@ export default function EbenenSettingsDialog({
|
|||||||
textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||||
Ebene
|
Ebene
|
||||||
</span>
|
</span>
|
||||||
<select
|
<div style={{ flex: 1, display: 'flex', minWidth: 0 }}>
|
||||||
value={pickerSelected || draft.code}
|
<BarCombo
|
||||||
onChange={(ev) => onPickEbene && onPickEbene(ev.target.value, draft)}
|
value={pickerSelected || draft.code}
|
||||||
style={{ flex: 1, fontSize: 11, minWidth: 0,
|
onChange={(v) => onPickEbene && onPickEbene(v, draft)}
|
||||||
fontFamily: 'var(--font-mono)' }}
|
title="Zwischen Ebenen wechseln — aktuelle Änderungen werden mit übernommen"
|
||||||
title="Zwischen Ebenen wechseln — aktuelle Änderungen werden mit übernommen"
|
>
|
||||||
>
|
{pickerEbenen.map(e => (
|
||||||
{pickerEbenen.map(e => (
|
<option key={e.code} value={e.code}>
|
||||||
<option key={e.code} value={e.code}>
|
{e.code} — {e.name}
|
||||||
{e.code} — {e.name}
|
</option>
|
||||||
</option>
|
))}
|
||||||
))}
|
</BarCombo>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : !embedded && (
|
) : !embedded && (
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
@@ -51,9 +51,8 @@ function ZeichnungsebeneRow({
|
|||||||
padding: '1px 12px',
|
padding: '1px 12px',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
|
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
|
||||||
borderRadius: 0,
|
borderRadius: active ? 999 : 0,
|
||||||
borderLeft: '3px solid ' + (active ? 'var(--accent)' : 'transparent'),
|
borderBottom: active ? '1px solid transparent' : '1px solid var(--border-light)',
|
||||||
borderBottom: '1px solid var(--border-light)',
|
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
minHeight: 24,
|
minHeight: 24,
|
||||||
@@ -67,7 +66,7 @@ function ZeichnungsebeneRow({
|
|||||||
><Icon name={eyeIcon} size={12} /></button>
|
><Icon name={eyeIcon} size={12} /></button>
|
||||||
|
|
||||||
<span style={{
|
<span style={{
|
||||||
fontWeight: active ? 700 : 500,
|
fontWeight: 500,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: active ? 'var(--active-light)' : 'var(--text-label)',
|
color: active ? 'var(--active-light)' : 'var(--text-label)',
|
||||||
flex: 1, minWidth: 0,
|
flex: 1, minWidth: 0,
|
||||||
@@ -128,9 +127,6 @@ export default function GeschossManager({
|
|||||||
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
||||||
|
|
||||||
const sorted = [...zeichnungsebenen].reverse()
|
const sorted = [...zeichnungsebenen].reverse()
|
||||||
const gesamthoehe = zeichnungsebenen
|
|
||||||
.filter(z => z.isGeschoss)
|
|
||||||
.reduce((s, z) => s + (z.hoehe ?? 0), 0)
|
|
||||||
|
|
||||||
const addQuick = () => {
|
const addQuick = () => {
|
||||||
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
|
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
|
||||||
@@ -235,19 +231,6 @@ export default function GeschossManager({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
|
||||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
||||||
padding: '3px 14px',
|
|
||||||
background: 'var(--bg-section)',
|
|
||||||
borderBottom: '1px solid var(--border-light)',
|
|
||||||
}}>
|
|
||||||
<span className="label-xs">Gebäudehöhe</span>
|
|
||||||
<span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)' }}>
|
|
||||||
{gesamthoehe.toFixed(2)} m
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
|
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
|
||||||
EbenenManager). */}
|
EbenenManager). */}
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
@@ -170,6 +170,11 @@ export function setOverridesPreset(name) { send('SET_OVERRIDES_PRESET', { name:
|
|||||||
export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) }
|
export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) }
|
||||||
export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
|
export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
|
||||||
export function openKameraPanel() { send('OPEN_KAMERA_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 openAbout() { send('OPEN_ABOUT', {}) }
|
||||||
export function createText() { send('CREATE_TEXT', {}) }
|
export function createText() { send('CREATE_TEXT', {}) }
|
||||||
export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) }
|
export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) }
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import OverridesApp from './OverridesApp.jsx'
|
|||||||
import DimensionenApp from './DimensionenApp.jsx'
|
import DimensionenApp from './DimensionenApp.jsx'
|
||||||
import LayoutsApp from './LayoutsApp.jsx'
|
import LayoutsApp from './LayoutsApp.jsx'
|
||||||
import ElementeApp from './ElementeApp.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 mode = (typeof window !== 'undefined' && window.PANEL_MODE) || 'ebenen'
|
||||||
const RootApp = mode === 'gestaltung' ? GestaltungApp
|
const RootApp = mode === 'gestaltung' ? GestaltungApp
|
||||||
@@ -48,6 +50,8 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
|
|||||||
: mode === 'masse_settings' ? MasseSettingsApp
|
: mode === 'masse_settings' ? MasseSettingsApp
|
||||||
: mode === 'about' ? AboutApp
|
: mode === 'about' ? AboutApp
|
||||||
: mode === 'text_editor' ? TextEditorApp
|
: mode === 'text_editor' ? TextEditorApp
|
||||||
|
: mode === 'elemente_uebersicht' ? ElementeUebersichtApp
|
||||||
|
: mode === 'elemente_properties' ? ElementePropertiesApp
|
||||||
: App
|
: App
|
||||||
|
|
||||||
window.onerror = function (msg, src, line, col, err) {
|
window.onerror = function (msg, src, line, col, err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user