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:
+467
-21
@@ -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_SIA = "dossier_raum_sia" # "" | "hnf" | "nnf" | "vf" | "ff"
|
||||
_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")
|
||||
|
||||
@@ -407,6 +435,40 @@ def _list_hatch_patterns(doc):
|
||||
print("[ELEMENTE] list_hatch_patterns:", ex)
|
||||
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 = {
|
||||
"s": [0.15, 0.20, 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_rundung=None, raum_txt_h=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_chain_members=None,
|
||||
aussp_parent=None):
|
||||
@@ -2694,6 +2762,44 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
|
||||
else:
|
||||
v = str(raum_fuellung)
|
||||
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
|
||||
if wand_layered is not None:
|
||||
obj_attrs.SetUserString(_KEY_WAND_LAYERED,
|
||||
@@ -2870,6 +2976,39 @@ def _read_meta(obj):
|
||||
if r_fuell_raw == "1": r_fuell = "Solid"
|
||||
elif r_fuell_raw == "0": r_fuell = ""
|
||||
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
|
||||
w_layered = (a.GetUserString(_KEY_WAND_LAYERED) == "1")
|
||||
w_layers_raw = a.GetUserString(_KEY_WAND_LAYERS) or ""
|
||||
@@ -2965,6 +3104,17 @@ def _read_meta(obj):
|
||||
"raum_align": r_align,
|
||||
"raum_sia": r_sia,
|
||||
"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_layers": w_layers,
|
||||
"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,
|
||||
text_height, z=0.0, align="mid"):
|
||||
"""Baut eine TextEntity am Centroid: 'Nummer Name\nA m^2'.
|
||||
align: 'links' | 'mid' | 'rechts' — wirkt auf die Justification."""
|
||||
text_height, z=0.0, align="mid",
|
||||
font=None, bold=False, italic=False,
|
||||
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:
|
||||
plane = rg.Plane(rg.Point3d(centroid.X, centroid.Y, float(z)),
|
||||
rg.Vector3d.ZAxis)
|
||||
te = rg.TextEntity()
|
||||
# Zeile 1: Nummer + Name (falls vorhanden), sonst nur Name
|
||||
line1 = (name or "Raum").strip()
|
||||
if nummer and str(nummer).strip():
|
||||
line1 = "{} {}".format(str(nummer).strip(), line1)
|
||||
# Zeile 2: Flaeche
|
||||
area_line = "{} m²".format(_format_area(area, rundung))
|
||||
te.Text = "{}\n{}".format(line1, area_line)
|
||||
|
||||
# Field-Value-Resolver — gibt den anzuzeigenden String fuer eine
|
||||
# Field-ID oder None wenn das Feld leer/inaktiv ist.
|
||||
def _field_value(fid):
|
||||
if fid == "nummer":
|
||||
v = (nummer or "").strip()
|
||||
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 "{} m²".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
|
||||
try: te.TextHeight = float(text_height)
|
||||
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
|
||||
else: te.Justification = rg.TextJustification.MiddleCenter
|
||||
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
|
||||
except Exception as 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:
|
||||
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(
|
||||
ctr,
|
||||
stamp_pt,
|
||||
meta.get("raum_name", "Raum"),
|
||||
meta.get("raum_nummer", ""),
|
||||
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),
|
||||
meta.get("raum_txt_h", 0.20),
|
||||
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:
|
||||
return True # Outline evtl. offen — Source behalten
|
||||
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_align=meta.get("raum_align"),
|
||||
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)
|
||||
except Exception as ex: print("[ELEMENTE] Raum AddText:", ex)
|
||||
return True
|
||||
@@ -6269,7 +6532,16 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
"txtH": meta.get("raum_txt_h", 0.20),
|
||||
"align": meta.get("raum_align", "mid"),
|
||||
"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,
|
||||
"areaFmt": _format_area(area, rnd_eff),
|
||||
"umfang": perim,
|
||||
@@ -6320,6 +6592,7 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
"activeGeschossName": _active_geschoss_name(doc),
|
||||
"siaFillMode": _sia_fill_enabled(doc),
|
||||
"hatchPatterns": _list_hatch_patterns(doc),
|
||||
"fonts": _list_system_fonts(),
|
||||
"materials": [
|
||||
{"name": n, "color": m["color"]}
|
||||
for n, m in _get_all_materials(doc).items()],
|
||||
@@ -9747,6 +10020,24 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
r_fuell = ""
|
||||
else:
|
||||
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"])
|
||||
attrs = axis_obj.Attributes
|
||||
if gstart != old_meta["geschoss"]:
|
||||
@@ -9762,13 +10053,30 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
raum_txt_h=r_th,
|
||||
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)
|
||||
axis_obj.Attributes = attrs
|
||||
axis_obj.CommitChanges()
|
||||
_save_last(raum_name_last=r_name, raum_rundung=r_rnd,
|
||||
raum_funktion=r_fkt, raum_txt_h=r_th,
|
||||
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)
|
||||
doc.Views.Redraw()
|
||||
self._send_state()
|
||||
@@ -10990,9 +11298,13 @@ _PAIRED_VOLUME_TYPES = (
|
||||
"wand_volume", "decke_volume", "dach_volume",
|
||||
"oeffnung_volume", "treppe_volume",
|
||||
"stuetze_volume", "traeger_volume",
|
||||
# Raum: Stempel-Text + SIA-Fuellung haengen an raum_outline. Damit die
|
||||
# drei gemeinsam markiert + via Rhino-Move zusammen verschoben werden.
|
||||
"raum_stamp", "raum_fill",
|
||||
# Raum-Fuellung haengt an der Outline und soll mitwandern → pairing aktiv.
|
||||
# raum_stamp ABSICHTLICH NICHT hier: Klick auf den Stempel-Text soll nur
|
||||
# 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 = (
|
||||
"wand_axis", "decke_outline", "dach_outline",
|
||||
@@ -11561,10 +11873,17 @@ def _snapshot_source_positions(doc):
|
||||
"pos": (p.X, p.Y, p.Z)}
|
||||
elif isinstance(geom, rg.Curve):
|
||||
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,
|
||||
"oeff_parent": parent,
|
||||
"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:
|
||||
try:
|
||||
bb = geom.GetBoundingBox(True)
|
||||
@@ -11716,6 +12035,107 @@ def _on_command_begin(sender, e):
|
||||
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):
|
||||
# Bulk-Op fertig: RedrawEnabled zurueck + EINMAL redrawn + selection
|
||||
# refresh ans Gestaltung-Panel.
|
||||
@@ -11783,6 +12203,17 @@ def _on_command_end(sender, e):
|
||||
sc.sticky["_dossier_undo_serial"] = None
|
||||
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
|
||||
# User-Klick auf False gesetzt (`_suppress_redraw_until_cmd_end`). Den
|
||||
# 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)
|
||||
pid = m.get("oeff_parent")
|
||||
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:
|
||||
print("[ELEMENTE] post-cmd source:", ex)
|
||||
finally:
|
||||
|
||||
+227
-14
@@ -506,7 +506,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
|
||||
|
||||
// PropertiesView: gemeinsame Komponente, rendert die passende Property-
|
||||
// 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
|
||||
const upd = (p) => updateElement(selected.id, p)
|
||||
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')
|
||||
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')
|
||||
return <AussparungProperties aussp={selected} onDelete={del('Aussparung')} />
|
||||
// fenster/tuer
|
||||
@@ -580,6 +582,7 @@ export default function ElementeApp() {
|
||||
geschosse={geschosse}
|
||||
materials={state.materials || []}
|
||||
hatchPatterns={state.hatchPatterns}
|
||||
fonts={state.fonts || []}
|
||||
oeffStyles={state.oeffStyles || []} />
|
||||
</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 [nummer, setNummer] = useState(raum.nummer || '')
|
||||
const [txtH, setTxtH] = useState(String(raum.txtH || 0.20))
|
||||
const [funktion, setFunktion] = useState(raum.funktion || '')
|
||||
useEffect(() => {
|
||||
setName(raum.name || 'Raum')
|
||||
setNummer(raum.nummer || '')
|
||||
setTxtH(String(raum.txtH || 0.20))
|
||||
}, [raum.id, raum.name, raum.nummer, raum.txtH])
|
||||
setFunktion(raum.funktion || '')
|
||||
}, [raum.id, raum.name, raum.nummer, raum.funktion])
|
||||
|
||||
// Aktueller Wert von raum_fuellung: "" | "Solid" | "Hatch1" | … | "ByLayer"
|
||||
const fuell = raum.fuellung || ''
|
||||
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 (
|
||||
<div style={{
|
||||
@@ -837,17 +1028,39 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Texthöhe</span>
|
||||
<input type="text" value={txtH}
|
||||
onChange={(e) => setTxtH(e.target.value)}
|
||||
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Funktion</span>
|
||||
<input type="text" value={funktion}
|
||||
onChange={(e) => setFunktion(e.target.value)}
|
||||
onBlur={() => {
|
||||
const v = parseFloat(txtH)
|
||||
if (v > 0 && v !== raum.txtH) onUpdate({ txtH: v })
|
||||
else setTxtH(String(raum.txtH))
|
||||
const v = (funktion || '').trim()
|
||||
if (v !== (raum.funktion || '')) onUpdate({ funktion: v })
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
|
||||
style={{ flex: 1, fontSize: 11,
|
||||
fontFamily: 'DM Mono, monospace' }} />
|
||||
placeholder="z.B. Wohnen, Bad, Büro …"
|
||||
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 style={{
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function ElementePropertiesApp() {
|
||||
geschosse={state.geschosse || []}
|
||||
materials={state.materials || []}
|
||||
hatchPatterns={state.hatchPatterns}
|
||||
fonts={state.fonts || []}
|
||||
oeffStyles={state.oeffStyles || []}
|
||||
/>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user