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_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 = "{}".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 "{}".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: