diff --git a/rhino/elemente.py b/rhino/elemente.py
index 7de99f4..6f63daf 100644
--- a/rhino/elemente.py
+++ b/rhino/elemente.py
@@ -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:
diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx
index 982503e..7584b8a 100644
--- a/src/ElementeApp.jsx
+++ b/src/ElementeApp.jsx
@@ -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