Raumstempel: Drag-Drop-Layout + persistente Position + Oberleiste-Sync

Datenmodell auf der raum_outline:
- dossier_raum_stamp_dx/dy: Stempel-Offset zum Outline-Centroid
- dossier_raum_layout: JSON-Array of-Rows fuer Multi-Field-pro-Zeile
- dossier_raum_txt_font/bold/italic + raum_show_*: Typografie-Overrides
- Legacy show_*-Flags bleiben Fallback wenn kein Layout gesetzt

Backend:
- _make_raum_stamp_text: Layout-Renderer (Rows zu Lines), Offset wird in
  _regenerate_element_body auf den Centroid addiert
- _sync_raum_stamps_to_source: laeuft am Anfang von _on_command_end,
  spiegelt aktuelle Stempel-Position + Font/Size/Style auf die Source
  zurueck → User-Edits via Move/Oberleiste/Properties ueberleben Regen
- _list_system_fonts: System-Fonts fuer Frontend-Dropdown
- Raumstempel-Bug-Fix: raum_outline jetzt in _on_command_end Regen-Pfad
  per Length-Check getriggert, snapshot um length erweitert. Vertex-Drag
  aktualisiert Flaechen-Wert. Outline-Sources fuegen affected_walls hinzu.
- raum_stamp aus _PAIRED_VOLUME_TYPES entfernt → einzeln greifbar/
  verschiebbar; Klick auf Outline pairt weiter alles drei.

Frontend (ElementeApp.jsx):
- StempelLayoutBuilder: HTML5 drag-and-drop UI, verfuegbare Felder als
  Pills oben (Klick = neue Row, Drag = in bestehende Row), bestehende
  Rows als drop-targets, × zum Entfernen
- Typografie-Block raus aus RaumProperties; Hinweis-Text auf Oberleiste
- PropertiesView nimmt jetzt fonts={state.fonts} (auch Satellite-Window)
This commit is contained in:
2026-05-26 20:42:32 +02:00
parent 02a00a9b4a
commit 01b6501a0c
3 changed files with 695 additions and 35 deletions
+467 -21
View File
@@ -297,6 +297,34 @@ _KEY_RAUM_TXT_H = "dossier_raum_txt_h" # Texthoehe in m
_KEY_RAUM_ALIGN = "dossier_raum_align" # "links"|"mid"|"rechts" _KEY_RAUM_ALIGN = "dossier_raum_align" # "links"|"mid"|"rechts"
_KEY_RAUM_SIA = "dossier_raum_sia" # "" | "hnf" | "nnf" | "vf" | "ff" _KEY_RAUM_SIA = "dossier_raum_sia" # "" | "hnf" | "nnf" | "vf" | "ff"
_KEY_RAUM_FUELL = "dossier_raum_fuellung" # "" (keine) | "Solid" | Pattern-Name | "ByLayer" _KEY_RAUM_FUELL = "dossier_raum_fuellung" # "" (keine) | "Solid" | Pattern-Name | "ByLayer"
# Stempel-Typografie (per-Raum-Override) — leer = Doc-Default-Font, Defaults siehe _read_meta
_KEY_RAUM_TXT_FONT = "dossier_raum_txt_font" # Font-Quartet-Name (z.B. "DM Mono", "Helvetica")
_KEY_RAUM_TXT_BOLD = "dossier_raum_txt_bold" # "0"|"1"
_KEY_RAUM_TXT_ITAL = "dossier_raum_txt_italic" # "0"|"1"
# Sichtbare Felder im Stempel — alle default "1" ausser SIA ("0"). Empty
# UserString wird wie "1" behandelt, damit alte Raeume ohne Migrate die
# bisherige Anzeige behalten.
_KEY_RAUM_SHOW_NUM = "dossier_raum_show_nummer"
_KEY_RAUM_SHOW_NAM = "dossier_raum_show_name"
_KEY_RAUM_SHOW_FKT = "dossier_raum_show_funktion"
_KEY_RAUM_SHOW_ARE = "dossier_raum_show_area"
_KEY_RAUM_SHOW_SIA = "dossier_raum_show_sia"
# Stempel-Position als Offset zum Outline-Centroid (m). Persistiert vom
# Auto-Sync in _on_command_end — User-Move des Stempels wird so beim
# naechsten Outline-Modify nicht wieder auf den Centroid zurueck-snapped.
_KEY_RAUM_STAMP_DX = "dossier_raum_stamp_dx"
_KEY_RAUM_STAMP_DY = "dossier_raum_stamp_dy"
# Field-Layout als JSON-Array von Rows. Jede Row ist eine Liste von
# Field-IDs. Z.B. [["nummer","name","area"],["funktion"]] → eine Zeile mit
# Nummer+Name+Flaeche, dann Funktion auf eigener Zeile. Leer = legacy
# show_*-Flags benutzen.
_KEY_RAUM_LAYOUT = "dossier_raum_layout"
# Verfuegbare Field-IDs fuers Layout
_RAUM_FIELD_IDS = ("nummer", "name", "funktion", "area", "sia")
# Default-Layout (entspricht dem alten Verhalten)
_RAUM_LAYOUT_DEFAULT = [["nummer", "name"], ["funktion"], ["area"]]
_RAUM_RUNDUNGEN = ("exakt", "0.01", "0.1", "0.5", "1") _RAUM_RUNDUNGEN = ("exakt", "0.01", "0.1", "0.5", "1")
@@ -407,6 +435,40 @@ def _list_hatch_patterns(doc):
print("[ELEMENTE] list_hatch_patterns:", ex) print("[ELEMENTE] list_hatch_patterns:", ex)
return out return out
def _list_system_fonts():
"""Sammelt installierte Font-Quartet-Namen via Rhino.DocObjects.Font.
Liefert sortierte Liste mit den haeufig benutzten DOSSIER-Fonts oben."""
out = []
preferred = ["DM Mono", "Krungthep", "Archivo Black", "Playfair Display",
"Helvetica", "Helvetica Neue", "Arial", "SF Pro Display",
"Times New Roman", "Courier New"]
try:
from Rhino.DocObjects import Font as _Font
installed = _Font.InstalledFontsAsString()
# InstalledFontsAsString liefert ; oder \n separated — beide handeln
raw = []
if installed:
for sep in (";", "\n", "\r"):
if sep in installed:
raw = [s.strip() for s in installed.split(sep) if s.strip()]
break
if not raw: raw = [installed.strip()]
seen = set()
# Erst Preferred wenn installiert
for f in preferred:
if f in raw and f not in seen:
out.append(f); seen.add(f)
# Dann der Rest alphabetisch
for f in sorted(raw):
if f and f not in seen:
out.append(f); seen.add(f)
except Exception as ex:
print("[ELEMENTE] list_system_fonts:", ex)
# Fallback: nur die DOSSIER-Defaults
out = preferred[:]
return out
_TREPPE_SOLL_DEFAULT = { _TREPPE_SOLL_DEFAULT = {
"s": [0.15, 0.20, True], "s": [0.15, 0.20, True],
"a": [0.21, 0.35, True], "a": [0.21, 0.35, True],
@@ -2530,6 +2592,12 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
raum_name=None, raum_nummer=None, raum_funktion=None, raum_name=None, raum_nummer=None, raum_funktion=None,
raum_rundung=None, raum_txt_h=None, raum_rundung=None, raum_txt_h=None,
raum_align=None, raum_sia=None, raum_fuellung=None, raum_align=None, raum_sia=None, raum_fuellung=None,
raum_txt_font=None, raum_txt_bold=None, raum_txt_italic=None,
raum_show_nummer=None, raum_show_name=None,
raum_show_funktion=None, raum_show_area=None,
raum_show_sia=None,
raum_stamp_dx=None, raum_stamp_dy=None,
raum_layout=None,
wand_layered=None, wand_layers=None, wand_layer_idx=None, wand_layered=None, wand_layers=None, wand_layer_idx=None,
wand_chain_members=None, wand_chain_members=None,
aussp_parent=None): aussp_parent=None):
@@ -2694,6 +2762,44 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
else: else:
v = str(raum_fuellung) v = str(raum_fuellung)
obj_attrs.SetUserString(_KEY_RAUM_FUELL, v) obj_attrs.SetUserString(_KEY_RAUM_FUELL, v)
# Stempel-Typografie + Sichtbarkeit (per-Raum-Override)
if raum_txt_font is not None:
obj_attrs.SetUserString(_KEY_RAUM_TXT_FONT, str(raum_txt_font))
if raum_txt_bold is not None:
obj_attrs.SetUserString(_KEY_RAUM_TXT_BOLD,
"1" if bool(raum_txt_bold) else "0")
if raum_txt_italic is not None:
obj_attrs.SetUserString(_KEY_RAUM_TXT_ITAL,
"1" if bool(raum_txt_italic) else "0")
for key, val in (
(_KEY_RAUM_SHOW_NUM, raum_show_nummer),
(_KEY_RAUM_SHOW_NAM, raum_show_name),
(_KEY_RAUM_SHOW_FKT, raum_show_funktion),
(_KEY_RAUM_SHOW_ARE, raum_show_area),
(_KEY_RAUM_SHOW_SIA, raum_show_sia),
):
if val is not None:
obj_attrs.SetUserString(key, "1" if bool(val) else "0")
if raum_stamp_dx is not None:
try: obj_attrs.SetUserString(_KEY_RAUM_STAMP_DX,
"{:.6f}".format(float(raum_stamp_dx)))
except Exception: pass
if raum_stamp_dy is not None:
try: obj_attrs.SetUserString(_KEY_RAUM_STAMP_DY,
"{:.6f}".format(float(raum_stamp_dy)))
except Exception: pass
if raum_layout is not None:
try:
import json as _json
if isinstance(raum_layout, str):
# validate as JSON
_json.loads(raum_layout)
obj_attrs.SetUserString(_KEY_RAUM_LAYOUT, raum_layout)
elif isinstance(raum_layout, list):
obj_attrs.SetUserString(_KEY_RAUM_LAYOUT,
_json.dumps(raum_layout))
except Exception as ex:
print("[ELEMENTE] raum_layout set:", ex)
# Wand-Schichten # Wand-Schichten
if wand_layered is not None: if wand_layered is not None:
obj_attrs.SetUserString(_KEY_WAND_LAYERED, obj_attrs.SetUserString(_KEY_WAND_LAYERED,
@@ -2870,6 +2976,39 @@ def _read_meta(obj):
if r_fuell_raw == "1": r_fuell = "Solid" if r_fuell_raw == "1": r_fuell = "Solid"
elif r_fuell_raw == "0": r_fuell = "" elif r_fuell_raw == "0": r_fuell = ""
else: r_fuell = r_fuell_raw or "" else: r_fuell = r_fuell_raw or ""
# Stempel-Typografie (Defaults: leer = Doc-Default-Font, bold/italic off)
r_font = a.GetUserString(_KEY_RAUM_TXT_FONT) or ""
r_bold = (a.GetUserString(_KEY_RAUM_TXT_BOLD) == "1")
r_ital = (a.GetUserString(_KEY_RAUM_TXT_ITAL) == "1")
# Sichtbarkeit — leer/None -> Default "1" (Backwards-Compat), ausser
# SIA (default "0"). Nur explizites "0" deaktiviert ein Feld.
def _show_default_on(key):
v = a.GetUserString(key)
return v != "0" # None, "", "1" -> True
r_show_num = _show_default_on(_KEY_RAUM_SHOW_NUM)
r_show_nam = _show_default_on(_KEY_RAUM_SHOW_NAM)
r_show_fkt = _show_default_on(_KEY_RAUM_SHOW_FKT)
r_show_are = _show_default_on(_KEY_RAUM_SHOW_ARE)
r_show_sia = (a.GetUserString(_KEY_RAUM_SHOW_SIA) == "1")
# Stempel-Position-Offset
try: r_stamp_dx = float(a.GetUserString(_KEY_RAUM_STAMP_DX) or "0")
except Exception: r_stamp_dx = 0.0
try: r_stamp_dy = float(a.GetUserString(_KEY_RAUM_STAMP_DY) or "0")
except Exception: r_stamp_dy = 0.0
# Field-Layout — parsed JSON list of rows
r_layout_raw = a.GetUserString(_KEY_RAUM_LAYOUT) or ""
r_layout = []
if r_layout_raw:
try:
import json as _json
parsed = _json.loads(r_layout_raw)
if isinstance(parsed, list):
for row in parsed:
if not isinstance(row, list): continue
clean = [str(f) for f in row
if isinstance(f, str) and f in _RAUM_FIELD_IDS]
if clean: r_layout.append(clean)
except Exception: r_layout = []
# Wand-Schichten # Wand-Schichten
w_layered = (a.GetUserString(_KEY_WAND_LAYERED) == "1") w_layered = (a.GetUserString(_KEY_WAND_LAYERED) == "1")
w_layers_raw = a.GetUserString(_KEY_WAND_LAYERS) or "" w_layers_raw = a.GetUserString(_KEY_WAND_LAYERS) or ""
@@ -2965,6 +3104,17 @@ def _read_meta(obj):
"raum_align": r_align, "raum_align": r_align,
"raum_sia": r_sia, "raum_sia": r_sia,
"raum_fuellung": r_fuell, "raum_fuellung": r_fuell,
"raum_txt_font": r_font,
"raum_txt_bold": r_bold,
"raum_txt_italic": r_ital,
"raum_show_nummer": r_show_num,
"raum_show_name": r_show_nam,
"raum_show_funktion": r_show_fkt,
"raum_show_area": r_show_are,
"raum_show_sia": r_show_sia,
"raum_stamp_dx": r_stamp_dx,
"raum_stamp_dy": r_stamp_dy,
"raum_layout": r_layout,
"wand_layered": w_layered, "wand_layered": w_layered,
"wand_layers": w_layers, "wand_layers": w_layers,
"wand_layer_idx": w_layer_idx, "wand_layer_idx": w_layer_idx,
@@ -4563,20 +4713,78 @@ def _format_area(area, rundung):
def _make_raum_stamp_text(centroid, name, nummer, funktion, area, rundung, def _make_raum_stamp_text(centroid, name, nummer, funktion, area, rundung,
text_height, z=0.0, align="mid"): text_height, z=0.0, align="mid",
"""Baut eine TextEntity am Centroid: 'Nummer Name\nA m^2'. font=None, bold=False, italic=False,
align: 'links' | 'mid' | 'rechts' wirkt auf die Justification.""" show_nummer=True, show_name=True,
show_funktion=True, show_area=True,
show_sia=False, sia_code="",
layout=None,
doc=None):
"""Baut eine TextEntity an `centroid` (= bereits offset-applizierte
Position, NICHT der reine Outline-Centroid). Layout bestimmt welche
Felder in welcher Zeile stehen Liste von Rows, jede Row eine Liste
von Field-IDs (nummer, name, funktion, area, sia). Wenn `layout`
leer/None: Fallback auf die show_*-Flags + Default-Reihenfolge.
"""
try: try:
plane = rg.Plane(rg.Point3d(centroid.X, centroid.Y, float(z)), plane = rg.Plane(rg.Point3d(centroid.X, centroid.Y, float(z)),
rg.Vector3d.ZAxis) rg.Vector3d.ZAxis)
te = rg.TextEntity() te = rg.TextEntity()
# Zeile 1: Nummer + Name (falls vorhanden), sonst nur Name
line1 = (name or "Raum").strip() # Field-Value-Resolver — gibt den anzuzeigenden String fuer eine
if nummer and str(nummer).strip(): # Field-ID oder None wenn das Feld leer/inaktiv ist.
line1 = "{} {}".format(str(nummer).strip(), line1) def _field_value(fid):
# Zeile 2: Flaeche if fid == "nummer":
area_line = "{}".format(_format_area(area, rundung)) v = (nummer or "").strip()
te.Text = "{}\n{}".format(line1, area_line) return v or None
if fid == "name":
v = (name or "").strip()
return v or None
if fid == "funktion":
v = (funktion or "").strip()
return v or None
if fid == "area":
return "{}".format(_format_area(area, rundung))
if fid == "sia":
tag = _SIA_LABELS.get(sia_code or "", "")
return tag if (tag and tag != "") else None
return None
# Layout resolven: explizit gesetzt > show_*-Flags
if layout and isinstance(layout, list) and any(layout):
rows = layout
else:
# Aus show_*-Flags ein implizites Layout bauen
rows = []
head = []
if show_nummer: head.append("nummer")
if show_name: head.append("name")
if head: rows.append(head)
if show_funktion: rows.append(["funktion"])
tail = []
if show_area: tail.append("area")
if show_sia: tail.append("sia")
if tail: rows.append(tail)
# Lines aus Rows bauen — pro Row die nicht-leeren Field-Values
# joinen, leere Rows weglassen.
lines = []
for row in rows:
parts = []
for fid in row:
v = _field_value(fid)
if v: parts.append(v)
if parts:
# Spezial: "area · sia" mit Mitten-Punkt wenn beide in Row
if (len(parts) == 2 and "area" in row and "sia" in row
and row.index("area") < row.index("sia")):
lines.append("{} · {}".format(parts[0], parts[1]))
else:
lines.append(" ".join(parts))
if not lines:
# Komplett leerer Stempel waere unsichtbar — Name als Fallback
lines.append((name or "Raum").strip())
te.Text = "\n".join(lines)
te.Plane = plane te.Plane = plane
try: te.TextHeight = float(text_height) try: te.TextHeight = float(text_height)
except Exception: te.TextHeight = 0.20 except Exception: te.TextHeight = 0.20
@@ -4585,6 +4793,37 @@ def _make_raum_stamp_text(centroid, name, nummer, funktion, area, rundung,
elif align == "rechts": te.Justification = rg.TextJustification.MiddleRight elif align == "rechts": te.Justification = rg.TextJustification.MiddleRight
else: te.Justification = rg.TextJustification.MiddleCenter else: te.Justification = rg.TextJustification.MiddleCenter
except Exception: pass except Exception: pass
# Font/Style — nur wenn doc + font_name vorhanden, sonst Doc-Default
if doc is not None and font:
try:
f = Rhino.DocObjects.Font.FromQuartetProperties(
str(font), bool(bold), bool(italic))
if f is not None:
idx = doc.Fonts.FindOrCreate(f.QuartetName,
bool(bold), bool(italic))
if idx >= 0:
try: te.FontIndex = idx
except Exception:
try: te.SetFontIndex(idx)
except Exception: pass
except Exception as ex:
print("[ELEMENTE] Raum Stamp Font:", ex)
elif doc is not None and (bold or italic):
# Kein expliziter Font, aber Stil gesetzt — auf Default-Font
# bold/italic anwenden
try:
cur_idx = te.FontIndex
if cur_idx >= 0 and cur_idx < doc.Fonts.Count:
cur_font = doc.Fonts[cur_idx]
idx = doc.Fonts.FindOrCreate(cur_font.QuartetName,
bool(bold), bool(italic))
if idx >= 0:
try: te.FontIndex = idx
except Exception:
try: te.SetFontIndex(idx)
except Exception: pass
except Exception as ex:
print("[ELEMENTE] Raum Stamp Style:", ex)
return te return te
except Exception as ex: except Exception as ex:
print("[ELEMENTE] Raum Stamp:", ex) print("[ELEMENTE] Raum Stamp:", ex)
@@ -5918,9 +6157,14 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
except Exception as ex: except Exception as ex:
print("[ELEMENTE] override re-apply:", ex) print("[ELEMENTE] override re-apply:", ex)
# Stempel # Stempel-Position: Centroid + persistierter User-Offset (dx,dy).
# So bleibt der Stempel wo der User ihn zuletzt gezogen hat, auch
# nach Outline-Vertex-Drag etc.
sx = float(meta.get("raum_stamp_dx", 0.0))
sy = float(meta.get("raum_stamp_dy", 0.0))
stamp_pt = rg.Point3d(ctr.X + sx, ctr.Y + sy, ctr.Z)
te = _make_raum_stamp_text( te = _make_raum_stamp_text(
ctr, stamp_pt,
meta.get("raum_name", "Raum"), meta.get("raum_name", "Raum"),
meta.get("raum_nummer", ""), meta.get("raum_nummer", ""),
meta.get("raum_funktion", ""), meta.get("raum_funktion", ""),
@@ -5928,7 +6172,18 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
_resolve_raum_rundung(meta, doc), _resolve_raum_rundung(meta, doc),
meta.get("raum_txt_h", 0.20), meta.get("raum_txt_h", 0.20),
z=z_uk, z=z_uk,
align=meta.get("raum_align", "mid")) align=meta.get("raum_align", "mid"),
font=meta.get("raum_txt_font", ""),
bold=meta.get("raum_txt_bold", False),
italic=meta.get("raum_txt_italic", False),
show_nummer=meta.get("raum_show_nummer", True),
show_name=meta.get("raum_show_name", True),
show_funktion=meta.get("raum_show_funktion", True),
show_area=meta.get("raum_show_area", True),
show_sia=meta.get("raum_show_sia", False),
sia_code=meta.get("raum_sia", ""),
layout=meta.get("raum_layout") or None,
doc=doc)
if te is None: if te is None:
return True # Outline evtl. offen — Source behalten return True # Outline evtl. offen — Source behalten
attrs = Rhino.DocObjects.ObjectAttributes() attrs = Rhino.DocObjects.ObjectAttributes()
@@ -5942,7 +6197,15 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
raum_txt_h=meta.get("raum_txt_h"), raum_txt_h=meta.get("raum_txt_h"),
raum_align=meta.get("raum_align"), raum_align=meta.get("raum_align"),
raum_sia=meta.get("raum_sia"), raum_sia=meta.get("raum_sia"),
raum_fuellung=meta.get("raum_fuellung")) raum_fuellung=meta.get("raum_fuellung"),
raum_txt_font=meta.get("raum_txt_font"),
raum_txt_bold=meta.get("raum_txt_bold"),
raum_txt_italic=meta.get("raum_txt_italic"),
raum_show_nummer=meta.get("raum_show_nummer"),
raum_show_name=meta.get("raum_show_name"),
raum_show_funktion=meta.get("raum_show_funktion"),
raum_show_area=meta.get("raum_show_area"),
raum_show_sia=meta.get("raum_show_sia"))
try: doc.Objects.AddText(te, attrs) try: doc.Objects.AddText(te, attrs)
except Exception as ex: print("[ELEMENTE] Raum AddText:", ex) except Exception as ex: print("[ELEMENTE] Raum AddText:", ex)
return True return True
@@ -6269,7 +6532,16 @@ class ElementeBridge(panel_base.BaseBridge):
"txtH": meta.get("raum_txt_h", 0.20), "txtH": meta.get("raum_txt_h", 0.20),
"align": meta.get("raum_align", "mid"), "align": meta.get("raum_align", "mid"),
"sia": meta.get("raum_sia", ""), "sia": meta.get("raum_sia", ""),
"fuellung": bool(meta.get("raum_fuellung", False)), "fuellung": meta.get("raum_fuellung", "") or "",
"font": meta.get("raum_txt_font", "") or "",
"bold": bool(meta.get("raum_txt_bold", False)),
"italic": bool(meta.get("raum_txt_italic", False)),
"showNummer": bool(meta.get("raum_show_nummer", True)),
"showName": bool(meta.get("raum_show_name", True)),
"showFunktion": bool(meta.get("raum_show_funktion", True)),
"showArea": bool(meta.get("raum_show_area", True)),
"showSia": bool(meta.get("raum_show_sia", False)),
"layout": meta.get("raum_layout") or [],
"area": area, "area": area,
"areaFmt": _format_area(area, rnd_eff), "areaFmt": _format_area(area, rnd_eff),
"umfang": perim, "umfang": perim,
@@ -6320,6 +6592,7 @@ class ElementeBridge(panel_base.BaseBridge):
"activeGeschossName": _active_geschoss_name(doc), "activeGeschossName": _active_geschoss_name(doc),
"siaFillMode": _sia_fill_enabled(doc), "siaFillMode": _sia_fill_enabled(doc),
"hatchPatterns": _list_hatch_patterns(doc), "hatchPatterns": _list_hatch_patterns(doc),
"fonts": _list_system_fonts(),
"materials": [ "materials": [
{"name": n, "color": m["color"]} {"name": n, "color": m["color"]}
for n, m in _get_all_materials(doc).items()], for n, m in _get_all_materials(doc).items()],
@@ -9747,6 +10020,24 @@ class ElementeBridge(panel_base.BaseBridge):
r_fuell = "" r_fuell = ""
else: else:
r_fuell = str(r_fuell) r_fuell = str(r_fuell)
# Stempel-Typografie + Sichtbarkeit (per-Patch oder vom alten meta)
r_font = p.get("font", old_meta.get("raum_txt_font", "") or "")
r_bold = bool(p.get("bold", old_meta.get("raum_txt_bold", False)))
r_ital = bool(p.get("italic", old_meta.get("raum_txt_italic", False)))
r_show_num = bool(p.get("showNummer",
old_meta.get("raum_show_nummer", True)))
r_show_nam = bool(p.get("showName",
old_meta.get("raum_show_name", True)))
r_show_fkt = bool(p.get("showFunktion",
old_meta.get("raum_show_funktion", True)))
r_show_are = bool(p.get("showArea",
old_meta.get("raum_show_area", True)))
r_show_sia = bool(p.get("showSia",
old_meta.get("raum_show_sia", False)))
# Layout: Liste-of-Rows aus Frontend (JSON-serializable). Wenn
# nicht im Patch → vom alten meta uebernehmen.
r_layout = p.get("layout", old_meta.get("raum_layout", []))
if not isinstance(r_layout, list): r_layout = []
gstart = p.get("geschoss", old_meta["geschoss"]) gstart = p.get("geschoss", old_meta["geschoss"])
attrs = axis_obj.Attributes attrs = axis_obj.Attributes
if gstart != old_meta["geschoss"]: if gstart != old_meta["geschoss"]:
@@ -9762,13 +10053,30 @@ class ElementeBridge(panel_base.BaseBridge):
raum_txt_h=r_th, raum_txt_h=r_th,
raum_align=r_align, raum_align=r_align,
raum_sia=r_sia, raum_sia=r_sia,
raum_fuellung=r_fuell) raum_fuellung=r_fuell,
raum_txt_font=r_font,
raum_txt_bold=r_bold,
raum_txt_italic=r_ital,
raum_show_nummer=r_show_num,
raum_show_name=r_show_nam,
raum_show_funktion=r_show_fkt,
raum_show_area=r_show_are,
raum_show_sia=r_show_sia,
raum_layout=r_layout)
axis_obj.Attributes = attrs axis_obj.Attributes = attrs
axis_obj.CommitChanges() axis_obj.CommitChanges()
_save_last(raum_name_last=r_name, raum_rundung=r_rnd, _save_last(raum_name_last=r_name, raum_rundung=r_rnd,
raum_funktion=r_fkt, raum_txt_h=r_th, raum_funktion=r_fkt, raum_txt_h=r_th,
raum_align=r_align, raum_sia=r_sia, raum_align=r_align, raum_sia=r_sia,
raum_fuellung=r_fuell) raum_fuellung=r_fuell,
raum_txt_font=r_font, raum_txt_bold=r_bold,
raum_txt_italic=r_ital,
raum_show_nummer=r_show_num,
raum_show_name=r_show_nam,
raum_show_funktion=r_show_fkt,
raum_show_area=r_show_are,
raum_show_sia=r_show_sia,
raum_layout=r_layout)
_regenerate_volume(doc, wall_id) _regenerate_volume(doc, wall_id)
doc.Views.Redraw() doc.Views.Redraw()
self._send_state() self._send_state()
@@ -10990,9 +11298,13 @@ _PAIRED_VOLUME_TYPES = (
"wand_volume", "decke_volume", "dach_volume", "wand_volume", "decke_volume", "dach_volume",
"oeffnung_volume", "treppe_volume", "oeffnung_volume", "treppe_volume",
"stuetze_volume", "traeger_volume", "stuetze_volume", "traeger_volume",
# Raum: Stempel-Text + SIA-Fuellung haengen an raum_outline. Damit die # Raum-Fuellung haengt an der Outline und soll mitwandern → pairing aktiv.
# drei gemeinsam markiert + via Rhino-Move zusammen verschoben werden. # raum_stamp ABSICHTLICH NICHT hier: Klick auf den Stempel-Text soll nur
"raum_stamp", "raum_fill", # den Text selektieren, damit der User ihn unabhaengig verschieben kann
# (Position bleibt via dx,dy-UserString persistent, siehe
# _sync_raum_stamps_to_source). Klick auf die Outline pairt weiter alles
# drei via _PAIRED_SOURCE_TYPES + _find_all_volumes.
"raum_fill",
) )
_PAIRED_SOURCE_TYPES = ( _PAIRED_SOURCE_TYPES = (
"wand_axis", "decke_outline", "dach_outline", "wand_axis", "decke_outline", "dach_outline",
@@ -11561,10 +11873,17 @@ def _snapshot_source_positions(doc):
"pos": (p.X, p.Y, p.Z)} "pos": (p.X, p.Y, p.Z)}
elif isinstance(geom, rg.Curve): elif isinstance(geom, rg.Curve):
s = geom.PointAtStart; e = geom.PointAtEnd s = geom.PointAtStart; e = geom.PointAtEnd
# length: change-Detection fuer Outline-Sources
# (raum_outline etc.) deren Start/End bei geschlossener
# PolyCurve auf demselben Punkt sitzen — Length aendert
# sich bei jedem Vertex-Drag, Start/End nicht.
try: length = float(geom.GetLength())
except Exception: length = 0.0
snap["sources"][m["id"]] = {"type": t, snap["sources"][m["id"]] = {"type": t,
"oeff_parent": parent, "oeff_parent": parent,
"start": (s.X, s.Y, s.Z), "start": (s.X, s.Y, s.Z),
"end": (e.X, e.Y, e.Z)} "end": (e.X, e.Y, e.Z),
"length": length}
elif t in VOLUME_TYPES: elif t in VOLUME_TYPES:
try: try:
bb = geom.GetBoundingBox(True) bb = geom.GetBoundingBox(True)
@@ -11716,6 +12035,107 @@ def _on_command_begin(sender, e):
sc.sticky["_dossier_undo_serial"] = None sc.sticky["_dossier_undo_serial"] = None
def _sync_raum_stamps_to_source(doc):
"""Sync raum_stamp TextEntities → raum_outline Source-UserStrings.
Greift NACH jedem User-Command (Move des Stempels, Aenderungen via
Rhino-Properties oder Oberleiste-Text-Block). Bevor der Regen laeuft
aktualisieren wir die Source so dass die naechste Stempel-Generierung
die User-Edits beibehaelt:
- Offset (dx,dy) = stamp_position - new_centroid Stempel bleibt
wo der User ihn gezogen hat, springt nicht auf den neuen Centroid.
- TextHeight aus dem Stempel raum_txt_h
- Font/Bold/Italic raum_txt_font/bold/italic
No-op fuer Raeume ohne Stempel (neu, noch nicht regenned).
"""
if doc is None: return
try:
# Erst alle raum_outlines + dazugehoerige raum_stamps sammeln
outlines = {} # eid -> (src_obj, meta)
stamps = {} # eid -> stamp_obj
for obj in doc.Objects:
try:
m = _read_meta(obj)
if not m: continue
t = m.get("type")
if t == "raum_outline":
outlines[m["id"]] = (obj, m)
elif t == "raum_stamp":
stamps[m["id"]] = obj
except Exception: pass
if not outlines: return
for eid, (src_obj, meta) in outlines.items():
stamp_obj = stamps.get(eid)
if stamp_obj is None: continue
try:
te = stamp_obj.Geometry
if te is None: continue
# Aktuelle Stempel-Position (TextEntity.Plane.Origin ist die
# Ankerposition — robuster als bbox.center bei Multi-Line-Texten
# mit Justification.MiddleCenter)
try:
pos = te.Plane.Origin
except Exception:
bb = stamp_obj.Geometry.GetBoundingBox(True)
if not bb.IsValid: continue
pos = bb.Center
# Aktueller Outline-Centroid
src_geom = src_obj.Geometry
if not isinstance(src_geom, rg.Curve): continue
_, _, ctr = _raum_amp(src_geom)
if ctr is None: continue
new_dx = pos.X - ctr.X
new_dy = pos.Y - ctr.Y
# Font/Style/Size aus der TextEntity lesen
try: new_h = float(te.TextHeight)
except Exception: new_h = float(meta.get("raum_txt_h", 0.20))
cur_font = None
try: cur_font = te.Font
except Exception: pass
try: new_face = cur_font.QuartetName if cur_font else ""
except Exception: new_face = ""
try: new_bold = bool(cur_font.Bold) if cur_font else False
except Exception: new_bold = False
try: new_ital = bool(cur_font.Italic) if cur_font else False
except Exception: new_ital = False
# Nur schreiben wenn was geaendert hat — vermeidet Modify-
# Storms bei jedem Idle-Tick.
old_dx = float(meta.get("raum_stamp_dx", 0.0))
old_dy = float(meta.get("raum_stamp_dy", 0.0))
old_h = float(meta.get("raum_txt_h", 0.20))
old_face = meta.get("raum_txt_font", "") or ""
old_bold = bool(meta.get("raum_txt_bold", False))
old_ital = bool(meta.get("raum_txt_italic", False))
changed = (
abs(new_dx - old_dx) > 1e-6 or
abs(new_dy - old_dy) > 1e-6 or
abs(new_h - old_h) > 1e-6 or
new_face != old_face or
new_bold != old_bold or
new_ital != old_ital
)
if not changed: continue
attrs = src_obj.Attributes.Duplicate()
attrs.SetUserString(_KEY_RAUM_STAMP_DX,
"{:.6f}".format(new_dx))
attrs.SetUserString(_KEY_RAUM_STAMP_DY,
"{:.6f}".format(new_dy))
attrs.SetUserString(_KEY_RAUM_TXT_H,
"{:.4f}".format(new_h))
attrs.SetUserString(_KEY_RAUM_TXT_FONT, new_face)
attrs.SetUserString(_KEY_RAUM_TXT_BOLD,
"1" if new_bold else "0")
attrs.SetUserString(_KEY_RAUM_TXT_ITAL,
"1" if new_ital else "0")
doc.Objects.ModifyAttributes(src_obj.Id, attrs, True)
except Exception as ex:
print("[ELEMENTE] sync stamp", eid, ":", ex)
except Exception as ex:
print("[ELEMENTE] _sync_raum_stamps_to_source:", ex)
def _on_command_end(sender, e): def _on_command_end(sender, e):
# Bulk-Op fertig: RedrawEnabled zurueck + EINMAL redrawn + selection # Bulk-Op fertig: RedrawEnabled zurueck + EINMAL redrawn + selection
# refresh ans Gestaltung-Panel. # refresh ans Gestaltung-Panel.
@@ -11783,6 +12203,17 @@ def _on_command_end(sender, e):
sc.sticky["_dossier_undo_serial"] = None sc.sticky["_dossier_undo_serial"] = None
return return
# Raum-Stempel → Source-Sync: VOR dem Pure-Transform/Regen-Pfad. Damit
# bleibt der User-Move des Stempels (oder Font-Edit via Oberleiste)
# auch wenn die Outline danach regennt wird.
try:
_was_sync = sc.sticky.get(_REGEN_BUSY, False)
sc.sticky[_REGEN_BUSY] = True
try: _sync_raum_stamps_to_source(doc)
finally: sc.sticky[_REGEN_BUSY] = _was_sync
except Exception as ex:
print("[ELEMENTE] stamp-sync:", ex)
# RedrawEnabled wurde idR schon beim ersten Object-Event nach dem # RedrawEnabled wurde idR schon beim ersten Object-Event nach dem
# User-Klick auf False gesetzt (`_suppress_redraw_until_cmd_end`). Den # User-Klick auf False gesetzt (`_suppress_redraw_until_cmd_end`). Den
# gemerkten prev-Wert lesen. Falls kein Event gefeuert hat (z.B. Move # gemerkten prev-Wert lesen. Falls kein Event gefeuert hat (z.B. Move
@@ -12279,6 +12710,21 @@ def _on_command_end(sender, e):
_apply_oeffnung_constraint(obj, m, pseudo) _apply_oeffnung_constraint(obj, m, pseudo)
pid = m.get("oeff_parent") pid = m.get("oeff_parent")
if pid: affected_walls.add(pid) if pid: affected_walls.add(pid)
elif t in ("raum_outline", "decke_outline", "dach_outline",
"decke_aussparung_outline", "treppe_axis"):
# Outline-Source modified? Length-Check faengt Vertex-
# Drag bei geschlossenen PolyCurves (Pure-Transform-Pfad
# abortet hier mit Identity-Transform, weil PointAtStart
# == PointAtEnd → Length-Diff ist der zuverlaessige
# Change-Indikator). Bei Raeumen muss der Stempel-Text
# neu gerechnet werden (Flaeche aendert sich).
geom = obj.Geometry
if not isinstance(geom, rg.Curve): continue
try: new_len = float(geom.GetLength())
except Exception: new_len = 0.0
old_len = old.get("length", new_len)
if abs(new_len - old_len) > 1e-6:
affected_walls.add(m["id"])
except Exception as ex: except Exception as ex:
print("[ELEMENTE] post-cmd source:", ex) print("[ELEMENTE] post-cmd source:", ex)
finally: finally:
+227 -14
View File
@@ -506,7 +506,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
// PropertiesView: gemeinsame Komponente, rendert die passende Property- // PropertiesView: gemeinsame Komponente, rendert die passende Property-
// Form je nach Element-Typ. Wiederverwendbar in Inline + Satellite-Window. // Form je nach Element-Typ. Wiederverwendbar in Inline + Satellite-Window.
export function PropertiesView({ selected, geschosse, materials, hatchPatterns, oeffStyles }) { export function PropertiesView({ selected, geschosse, materials, hatchPatterns, oeffStyles, fonts }) {
if (!selected) return null if (!selected) return null
const upd = (p) => updateElement(selected.id, p) const upd = (p) => updateElement(selected.id, p)
const del = (label) => () => { if (window.confirm(`${label} löschen?`)) deleteElement(selected.id) } const del = (label) => () => { if (window.confirm(`${label} löschen?`)) deleteElement(selected.id) }
@@ -528,7 +528,9 @@ export function PropertiesView({ selected, geschosse, materials, hatchPatterns,
} }
if (selected.kind === 'raum') if (selected.kind === 'raum')
return <RaumProperties raum={selected} geschosse={geschosse} return <RaumProperties raum={selected} geschosse={geschosse}
hatchPatterns={hatchPatterns} onUpdate={upd} onDelete={del('Raum')} /> hatchPatterns={hatchPatterns} fonts={fonts || []}
onUpdate={upd} onDelete={del('Raum')} />
if (selected.kind === 'aussparung') if (selected.kind === 'aussparung')
return <AussparungProperties aussp={selected} onDelete={del('Aussparung')} /> return <AussparungProperties aussp={selected} onDelete={del('Aussparung')} />
// fenster/tuer // fenster/tuer
@@ -580,6 +582,7 @@ export default function ElementeApp() {
geschosse={geschosse} geschosse={geschosse}
materials={state.materials || []} materials={state.materials || []}
hatchPatterns={state.hatchPatterns} hatchPatterns={state.hatchPatterns}
fonts={state.fonts || []}
oeffStyles={state.oeffStyles || []} /> oeffStyles={state.oeffStyles || []} />
</div> </div>
)} )}
@@ -711,19 +714,207 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
) )
} }
function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns }) { // Field-Definitionen fuer den Stempel-Layout-Builder. Symmetrisch zu
// _RAUM_FIELD_IDS im Backend (elemente.py).
const RAUM_LAYOUT_FIELDS = [
{ id: 'nummer', label: 'Nummer', icon: 'tag' },
{ id: 'name', label: 'Name', icon: 'label' },
{ id: 'funktion', label: 'Funktion', icon: 'category' },
{ id: 'area', label: 'Fläche', icon: 'square_foot' },
{ id: 'sia', label: 'SIA-Tag', icon: 'class' },
]
// Layout-Builder mit Drag-and-Drop. Rows = Textzeilen, Felder in einer
// Row stehen nebeneinander. Drag-Quelle ist ein "active field" Pill oder
// ein "verfuegbares" Pill. Drop-Ziel ist eine Row (= an die Row anhaengen)
// oder die neue-Row-Drop-Zone unten.
function StempelLayoutBuilder({ layout, availableFields, onChange }) {
const [dragging, setDragging] = useState(null) // { id, fromRow|null }
const FIELD_META = Object.fromEntries(RAUM_LAYOUT_FIELDS.map(f => [f.id, f]))
const removeFromLayout = (fid) => {
const next = layout.map(row => row.filter(f => f !== fid))
.filter(row => row.length > 0)
return next
}
const handleDragStart = (e, fid, fromRow) => {
setDragging({ id: fid, fromRow })
try { e.dataTransfer.effectAllowed = 'move' } catch (_) {}
try { e.dataTransfer.setData('text/plain', fid) } catch (_) {}
}
const handleDragEnd = () => setDragging(null)
const handleDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move' }
const handleDropOnRow = (e, rowIdx) => {
e.preventDefault()
if (!dragging) return
const next = removeFromLayout(dragging.id)
// Wenn die ge-droppte Source und das Ziel dieselbe Row ist → no-op
// (Within-Row-Reorder waere komplexer; ignorieren wir vorerst)
if (next[rowIdx]) next[rowIdx] = [...next[rowIdx], dragging.id]
else next.push([dragging.id])
onChange(next)
setDragging(null)
}
const handleDropOnNewRow = (e) => {
e.preventDefault()
if (!dragging) return
const next = removeFromLayout(dragging.id)
next.push([dragging.id])
onChange(next)
setDragging(null)
}
const handleRemove = (fid) => {
onChange(removeFromLayout(fid))
}
const handleAddFromAvailable = (fid) => {
onChange([...layout, [fid]])
}
const pillStyle = (isDragging) => ({
display: 'inline-flex', alignItems: 'center', gap: 4,
padding: '4px 8px', fontSize: 10,
background: isDragging ? 'var(--accent)' : 'var(--bg-input)',
color: isDragging ? 'var(--bg-panel)' : 'var(--text-primary)',
border: '1px solid var(--border)', borderRadius: 999,
cursor: 'grab', userSelect: 'none',
fontFamily: 'DM Mono, monospace',
})
const rowStyle = {
display: 'flex', flexWrap: 'wrap', gap: 4,
padding: '6px 8px',
background: 'var(--bg-panel)',
border: '1px dashed var(--border)', borderRadius: 'var(--r)',
minHeight: 28, alignItems: 'center',
}
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 6,
paddingTop: 6, borderTop: '1px dashed var(--border)',
}}>
<span style={{ ...labelXs, marginBottom: 0 }}>Stempel-Layout</span>
<div style={{ fontSize: 9, color: 'var(--text-muted)', marginBottom: 2 }}>
Drag Felder zwischen Zeilen eine Zeile = eine Textzeile im Stempel.
</div>
{/* Verfuegbare Felder (Drag-Quelle) */}
{availableFields.length > 0 && (
<div style={{
display: 'flex', flexWrap: 'wrap', gap: 4,
padding: '4px 0', marginBottom: 2,
}}>
<span style={{ fontSize: 9, color: 'var(--text-muted)',
alignSelf: 'center', marginRight: 4 }}>+</span>
{availableFields.map(f => (
<span key={f.id}
draggable
onDragStart={(e) => handleDragStart(e, f.id, null)}
onDragEnd={handleDragEnd}
onClick={() => handleAddFromAvailable(f.id)}
title={`${f.label} hinzufügen (klick) oder in Zeile ziehen`}
style={{
...pillStyle(dragging && dragging.id === f.id),
opacity: 0.65, cursor: 'pointer',
}}>
<Icon name={f.icon} size={11} />{f.label}
</span>
))}
</div>
)}
{/* Rows */}
{layout.map((row, ri) => (
<div key={ri}
onDragOver={handleDragOver}
onDrop={(e) => handleDropOnRow(e, ri)}
style={rowStyle}>
{row.map(fid => {
const meta = FIELD_META[fid] || { label: fid, icon: 'label' }
return (
<span key={fid}
draggable
onDragStart={(e) => handleDragStart(e, fid, ri)}
onDragEnd={handleDragEnd}
style={pillStyle(dragging && dragging.id === fid)}>
<Icon name={meta.icon} size={11} />
{meta.label}
<button onClick={() => handleRemove(fid)}
title="Entfernen"
style={{
background: 'transparent', border: 'none',
cursor: 'pointer', padding: 0, marginLeft: 2,
color: 'inherit', opacity: 0.6,
}}>
<Icon name="close" size={10} />
</button>
</span>
)
})}
<span style={{ flex: 1, fontSize: 9, color: 'var(--text-muted)',
textAlign: 'right' }}>Zeile {ri + 1}</span>
</div>
))}
{/* Neue-Row Drop-Zone (nur wenn was dragable ist) */}
<div
onDragOver={handleDragOver}
onDrop={handleDropOnNewRow}
style={{
fontSize: 9, color: 'var(--text-muted)', textAlign: 'center',
padding: '6px 0',
border: '1px dashed transparent',
borderColor: dragging ? 'var(--accent)' : 'var(--border)',
borderRadius: 'var(--r)',
background: dragging ? 'var(--bg-item-hover)' : 'transparent',
transition: 'all 0.15s',
}}>
{dragging ? '↓ Hier ablegen für neue Zeile' : 'Drop hier für neue Zeile'}
</div>
</div>
)
}
function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fonts }) {
const [name, setName] = useState(raum.name || 'Raum') const [name, setName] = useState(raum.name || 'Raum')
const [nummer, setNummer] = useState(raum.nummer || '') const [nummer, setNummer] = useState(raum.nummer || '')
const [txtH, setTxtH] = useState(String(raum.txtH || 0.20)) const [funktion, setFunktion] = useState(raum.funktion || '')
useEffect(() => { useEffect(() => {
setName(raum.name || 'Raum') setName(raum.name || 'Raum')
setNummer(raum.nummer || '') setNummer(raum.nummer || '')
setTxtH(String(raum.txtH || 0.20)) setFunktion(raum.funktion || '')
}, [raum.id, raum.name, raum.nummer, raum.txtH]) }, [raum.id, raum.name, raum.nummer, raum.funktion])
// Aktueller Wert von raum_fuellung: "" | "Solid" | "Hatch1" | … | "ByLayer" // Aktueller Wert von raum_fuellung: "" | "Solid" | "Hatch1" | … | "ByLayer"
const fuell = raum.fuellung || '' const fuell = raum.fuellung || ''
const patternList = hatchPatterns || [] const patternList = hatchPatterns || []
// Layout aus State, mit Fallback auf show_*-Flags (backwards-compat
// fuer Raeume ohne explizites Layout). Backend faellt ebenfalls auf
// dieselbe Konvention zurueck.
const rawLayout = Array.isArray(raum.layout) ? raum.layout : []
const layout = rawLayout.length > 0 ? rawLayout : (() => {
const head = []
if (raum.showNummer !== false) head.push('nummer')
if (raum.showName !== false) head.push('name')
const rows = []
if (head.length) rows.push(head)
if (raum.showFunktion !== false) rows.push(['funktion'])
const tail = []
if (raum.showArea !== false) tail.push('area')
if (raum.showSia) tail.push('sia')
if (tail.length) rows.push(tail)
return rows.length ? rows : [['name']]
})()
const usedFields = new Set(layout.flat())
const availableFields = RAUM_LAYOUT_FIELDS.filter(f => !usedFields.has(f.id))
return ( return (
<div style={{ <div style={{
@@ -837,17 +1028,39 @@ 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-secondary)', width: 60 }}>Texthöhe</span> <span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Funktion</span>
<input type="text" value={txtH} <input type="text" value={funktion}
onChange={(e) => setTxtH(e.target.value)} onChange={(e) => setFunktion(e.target.value)}
onBlur={() => { onBlur={() => {
const v = parseFloat(txtH) const v = (funktion || '').trim()
if (v > 0 && v !== raum.txtH) onUpdate({ txtH: v }) if (v !== (raum.funktion || '')) onUpdate({ funktion: v })
else setTxtH(String(raum.txtH))
}} }}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }} onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, placeholder="z.B. Wohnen, Bad, Büro …"
fontFamily: 'DM Mono, monospace' }} /> style={{ flex: 1, fontSize: 11 }} />
</div>
{/* Stempel-Layout — Drag-and-Drop. Jede Row ist eine Textzeile,
Felder innerhalb einer Row landen in derselben Zeile. Drag
zwischen Rows um umzuordnen. Klick auf Field oben fügt es in
eine eigene neue Row hinzu. */}
<StempelLayoutBuilder
layout={layout}
availableFields={availableFields}
onChange={(newLayout) => onUpdate({ layout: newLayout })} />
{/* Hinweis: Typografie (Font/Stil/Höhe) wird in der OBERLEISTE
gesetzt — den Stempel im Viewport anklicken. Aenderungen werden
automatisch auf die Raum-Outline gespiegelt damit sie beim
naechsten Regen erhalten bleiben. */}
<div style={{
fontSize: 9, color: 'var(--text-muted)',
paddingTop: 4, borderTop: '1px dashed var(--border)',
display: 'flex', alignItems: 'center', gap: 4,
}}>
<Icon name="info" size={11} style={{ color: 'var(--text-muted)' }} />
Typografie (Font/Stil/Höhe): Stempel im Viewport selektieren
Oberleiste.
</div> </div>
<div style={{ <div style={{
+1
View File
@@ -35,6 +35,7 @@ export default function ElementePropertiesApp() {
geschosse={state.geschosse || []} geschosse={state.geschosse || []}
materials={state.materials || []} materials={state.materials || []}
hatchPatterns={state.hatchPatterns} hatchPatterns={state.hatchPatterns}
fonts={state.fonts || []}
oeffStyles={state.oeffStyles || []} oeffStyles={state.oeffStyles || []}
/> />
) : ( ) : (