GF/AGF-Outlines: eigene Layer + minimaler Stempel/UI

Geschossflaeche (GF) und Aussengeschossflaeche (AGF) sind reine
Flaechen-Ausweisungs-Outlines — keine Raeume mit Name/Nummer/Funktion.

Backend:
- Neue Layer-Helpers _layer_path_raum_gf (61_GF) und _layer_path_raum_agf
  (62_AGF) — eigene Sublayer pro Geschoss, eigene Default-Farben
- _layer_path_for_raum_sia(doc, gname, sia): routet sia=gf → GF-Layer,
  sia=agf → AGF-Layer, sonst RAEUME-Layer (HNF/NNF/VF/FF/leer)
- _regenerate_element_body raum_outline-Branch nutzt das Routing →
  Source-Outline migriert automatisch auf den richtigen Layer
- _update_wall raum-Branch: bei SIA-Wechsel (z.B. HNF → GF) auch
  Layer-Migration
- _make_raum_stamp_text: bei sia=gf/agf default-layout
  override auf [["sia", "area"]] → Stempel zeigt nur "GF 234.5 m²" /
  "AGF 18.0 m²" ohne Nummer/Name/Funktion

Frontend (RaumProperties):
- Conditional isFlaeche = sia in (gf, agf)
- Versteckt bei isFlaeche: Stil-Picker, Nummer-Input, Name-Input,
  Funktion-Input, StempelLayoutBuilder
- Bleibt sichtbar: Geschoss, SIA-Tag-Selector, Fuellung, Rundung,
  Skala-Modus, Flaeche/Umfang-Footer
- Info-Zeile zeigt bei isFlaeche: "Flaechen-Outline (GF) auf eigenem
  Layer · Stempel zeigt nur GF + Flaeche"
This commit is contained in:
2026-05-27 00:24:57 +02:00
parent 2386366566
commit ac7b2f2ee5
2 changed files with 130 additions and 63 deletions
+55 -6
View File
@@ -1079,6 +1079,36 @@ def _layer_path_raum(doc, geschoss_name):
return "{}::{}".format(geschoss_name, sub)
def _layer_path_raum_gf(doc, geschoss_name):
"""Geschossflaeche-Outline — Sublayer 'GF' (Code 61). Separater Layer
weil GF-Outlines die ganze Wand-Topologie umfassen und NICHT zusammen
mit Nutzflaechen-Raeumen visualisiert/exportiert werden sollen."""
sub = _find_ebene_sublayer_name(doc, ["gf", "geschossfl"],
"61", "GF",
default_color="#a0a0a0", default_lw=0.18)
return "{}::{}".format(geschoss_name, sub)
def _layer_path_raum_agf(doc, geschoss_name):
"""Aussengeschossflaeche-Outline (Balkone/Terrassen) — Sublayer 'AGF'
(Code 62). Eigener Layer."""
sub = _find_ebene_sublayer_name(doc, ["agf", "aussen"],
"62", "AGF",
default_color="#90b090", default_lw=0.13)
return "{}::{}".format(geschoss_name, sub)
def _layer_path_for_raum_sia(doc, geschoss_name, sia_tag):
"""Routet auf den richtigen Layer je nach SIA-Klassifikation.
gf 61_GF, agf 62_AGF, alles andere (HNF/NNF/VF/FF/leer) 60_RAEUME.
Damit kann der User pro Geschoss die Geschossflaechen separat sichtbar/
unsichtbar schalten oder als eigenes Layout exportieren."""
s = (sia_tag or "").lower()
if s == "gf": return _layer_path_raum_gf(doc, geschoss_name)
if s == "agf": return _layer_path_raum_agf(doc, geschoss_name)
return _layer_path_raum(doc, geschoss_name)
def _layer_path_symbole(doc, geschoss_name, variant):
"""Symbol-Layer (Library-Items). variant = '2d' oder '3d'.
Pfad: <geschoss>::40_SYMBOLE::SYMBOLE_2D bzw. SYMBOLE_3D.
@@ -4907,8 +4937,16 @@ def _make_raum_stamp_text(centroid, name, nummer, funktion, area, rundung,
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):
# Layout resolven: explizit gesetzt > show_*-Flags > GF/AGF-Default
# Sonderfall GF/AGF: das sind reine Flaechen-Outlines (Geschoss-
# flaeche / Aussengeschossflaeche) — kein Raum-Inhalt. Stempel
# zeigt nur 'GF 234.5 m²' bzw. 'AGF 18.0 m²' — keine Nummer/
# Name/Funktion.
_is_flaeche = (sia_code or "").lower() in ("gf", "agf")
if _is_flaeche and not (layout and isinstance(layout, list)
and any(layout)):
rows = [["sia", "area"]]
elif layout and isinstance(layout, list) and any(layout):
rows = layout
else:
# Aus show_*-Flags ein implizites Layout bauen
@@ -6328,9 +6366,15 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
# Raum: Source = geschlossene Outline; Volumes = TextEntity (Stempel)
# + optional Brep-Fuellung (SIA-Modus). Geht NICHT durch den
# Brep-Replace-Pfad — wird direkt hier emittiert.
layer = _ensure_layer(doc, _layer_path_raum(doc, geschoss_name))
# Layer-Routing nach SIA-Klassifikation: gf → 61_GF, agf → 62_AGF,
# sonst 60_RAEUME (HNF/NNF/VF/FF/leer). So sind GF-Outlines auf
# eigenem Layer einzeln aus-/einblendbar.
_raum_sia = (meta.get("raum_sia") or "").lower()
layer = _ensure_layer(doc,
_layer_path_for_raum_sia(doc, geschoss_name, _raum_sia))
src_layer = layer
# Source-Outline ggf. auf richtigen Layer migrieren
# Source-Outline ggf. auf richtigen Layer migrieren (z.B. nach
# SIA-Tag-Wechsel HNF → GF)
try:
if src_layer >= 0 and src_obj.Attributes.LayerIndex != src_layer:
new_attrs = src_obj.Attributes.Duplicate()
@@ -10659,10 +10703,15 @@ class ElementeBridge(panel_base.BaseBridge):
old_modus, r_modus, scale, r_th))
gstart = p.get("geschoss", old_meta["geschoss"])
attrs = axis_obj.Attributes
if gstart != old_meta["geschoss"]:
# Layer-Routing: bei Geschoss-Wechsel ODER SIA-Wechsel (gf/agf
# haben eigene Sublayer) den richtigen Layer setzen.
old_sia = (old_meta.get("raum_sia") or "").lower()
new_sia = (r_sia or "").lower()
if gstart != old_meta["geschoss"] or new_sia != old_sia:
gs = _geschoss_by_id(doc, gstart)
gn = gs.get("name", "EG") if gs else "EG"
attrs.LayerIndex = _ensure_layer(doc, _layer_path_raum(doc, gn))
attrs.LayerIndex = _ensure_layer(doc,
_layer_path_for_raum_sia(doc, gn, new_sia))
_attach_meta(attrs, wall_id, "raum_outline",
gstart, 0.0, "", "", "mid",
raum_name=r_name,
+21 -3
View File
@@ -937,6 +937,10 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
// Texthoehe (raum.txtH) + Ausrichtung (raum.align) werden via Oberleiste
// gesetzt — kein Local-State noetig, Stil-Speichern liest direkt aus raum.
const txtModus = raum.txtModus || 'fix'
// Sonderfall: GF/AGF sind reine Flaechen-Outlines (keine Raum-Inhalte
// wie Name/Nummer/Funktion). UI wird darunter minimiert auf SIA-Tag +
// Skala + Fluessigkeits-Anzeige.
const isFlaeche = (raum.sia === 'gf' || raum.sia === 'agf')
useEffect(() => {
setName(raum.name || 'Raum')
setNummer(raum.nummer || '')
@@ -988,6 +992,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
{/* Stempel-Stil Preset-Picker — apply einer gespeicherten Visual-Vorlage
auf den Raum. "+ Aktuell speichern" frischt die Liste mit den
jetzigen Settings als neuen Stil. Stile sind per-Doc. */}
{!isFlaeche && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Stil</span>
<select value={activeStilId}
@@ -1003,6 +1008,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
{activeStilId && <option value="__delete__">🗑 Aktiven Stil löschen</option>}
</select>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Geschoss</span>
@@ -1013,6 +1019,8 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
</select>
</div>
{/* Nummer + Name nur bei normalen Raeumen (nicht GF/AGF-Flaechen) */}
{!isFlaeche && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Nummer</span>
<input type="text" value={nummer}
@@ -1024,7 +1032,9 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
style={{ flex: 1, fontSize: 11,
fontFamily: 'DM Mono, monospace' }} />
</div>
)}
{!isFlaeche && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Name</span>
<input type="text" value={name}
@@ -1037,6 +1047,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11 }} />
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Typ</span>
@@ -1100,6 +1111,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
</select>
</div>
{!isFlaeche && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Funktion</span>
<input type="text" value={funktion}
@@ -1112,15 +1124,20 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
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. */}
eine eigene neue Row hinzu.
Bei GF/AGF-Flaechen wird das ausgeblendet — Stempel zeigt da
automatisch nur den SIA-Tag + Flaeche (z.B. "GF · 234.5 m²"). */}
{!isFlaeche && (
<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
@@ -1132,8 +1149,9 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
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.
{isFlaeche
? `Flächen-Outline (${(raum.sia || '').toUpperCase()}) auf eigenem Layer · Stempel zeigt nur ${(raum.sia || '').toUpperCase()} + Fläche`
: 'Typografie (Font/Stil/Höhe): Stempel im Viewport selektieren → Oberleiste.'}
</div>
<div style={{