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) 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): def _layer_path_symbole(doc, geschoss_name, variant):
"""Symbol-Layer (Library-Items). variant = '2d' oder '3d'. """Symbol-Layer (Library-Items). variant = '2d' oder '3d'.
Pfad: <geschoss>::40_SYMBOLE::SYMBOLE_2D bzw. SYMBOLE_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 tag if (tag and tag != "") else None
return None return None
# Layout resolven: explizit gesetzt > show_*-Flags # Layout resolven: explizit gesetzt > show_*-Flags > GF/AGF-Default
if layout and isinstance(layout, list) and any(layout): # 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 rows = layout
else: else:
# Aus show_*-Flags ein implizites Layout bauen # 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) # Raum: Source = geschlossene Outline; Volumes = TextEntity (Stempel)
# + optional Brep-Fuellung (SIA-Modus). Geht NICHT durch den # + optional Brep-Fuellung (SIA-Modus). Geht NICHT durch den
# Brep-Replace-Pfad — wird direkt hier emittiert. # 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 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: try:
if src_layer >= 0 and src_obj.Attributes.LayerIndex != src_layer: if src_layer >= 0 and src_obj.Attributes.LayerIndex != src_layer:
new_attrs = src_obj.Attributes.Duplicate() new_attrs = src_obj.Attributes.Duplicate()
@@ -10659,10 +10703,15 @@ class ElementeBridge(panel_base.BaseBridge):
old_modus, r_modus, scale, r_th)) old_modus, r_modus, scale, r_th))
gstart = p.get("geschoss", old_meta["geschoss"]) gstart = p.get("geschoss", old_meta["geschoss"])
attrs = axis_obj.Attributes attrs = axis_obj.Attributes
if gstart != old_meta["geschoss"]: # 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) gs = _geschoss_by_id(doc, gstart)
gn = gs.get("name", "EG") if gs else "EG" 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", _attach_meta(attrs, wall_id, "raum_outline",
gstart, 0.0, "", "", "mid", gstart, 0.0, "", "", "mid",
raum_name=r_name, 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 // Texthoehe (raum.txtH) + Ausrichtung (raum.align) werden via Oberleiste
// gesetzt — kein Local-State noetig, Stil-Speichern liest direkt aus raum. // gesetzt — kein Local-State noetig, Stil-Speichern liest direkt aus raum.
const txtModus = raum.txtModus || 'fix' 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(() => { useEffect(() => {
setName(raum.name || 'Raum') setName(raum.name || 'Raum')
setNummer(raum.nummer || '') setNummer(raum.nummer || '')
@@ -988,6 +992,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
{/* Stempel-Stil Preset-Picker — apply einer gespeicherten Visual-Vorlage {/* Stempel-Stil Preset-Picker — apply einer gespeicherten Visual-Vorlage
auf den Raum. "+ Aktuell speichern" frischt die Liste mit den auf den Raum. "+ Aktuell speichern" frischt die Liste mit den
jetzigen Settings als neuen Stil. Stile sind per-Doc. */} jetzigen Settings als neuen Stil. Stile sind per-Doc. */}
{!isFlaeche && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Stil</span> <span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Stil</span>
<select value={activeStilId} <select value={activeStilId}
@@ -1003,6 +1008,7 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
{activeStilId && <option value="__delete__">🗑 Aktiven Stil löschen</option>} {activeStilId && <option value="__delete__">🗑 Aktiven Stil löschen</option>}
</select> </select>
</div> </div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Geschoss</span> <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> </select>
</div> </div>
{/* Nummer + Name nur bei normalen Raeumen (nicht GF/AGF-Flaechen) */}
{!isFlaeche && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Nummer</span> <span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Nummer</span>
<input type="text" value={nummer} <input type="text" value={nummer}
@@ -1024,7 +1032,9 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
style={{ flex: 1, fontSize: 11, style={{ flex: 1, fontSize: 11,
fontFamily: 'DM Mono, monospace' }} /> fontFamily: 'DM Mono, monospace' }} />
</div> </div>
)}
{!isFlaeche && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Name</span> <span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Name</span>
<input type="text" value={name} <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() }} onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11 }} /> style={{ flex: 1, fontSize: 11 }} />
</div> </div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Typ</span> <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> </select>
</div> </div>
{!isFlaeche && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Funktion</span> <span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Funktion</span>
<input type="text" value={funktion} <input type="text" value={funktion}
@@ -1112,15 +1124,20 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
placeholder="z.B. Wohnen, Bad, Büro …" placeholder="z.B. Wohnen, Bad, Büro …"
style={{ flex: 1, fontSize: 11 }} /> style={{ flex: 1, fontSize: 11 }} />
</div> </div>
)}
{/* Stempel-Layout — Drag-and-Drop. Jede Row ist eine Textzeile, {/* Stempel-Layout — Drag-and-Drop. Jede Row ist eine Textzeile,
Felder innerhalb einer Row landen in derselben Zeile. Drag Felder innerhalb einer Row landen in derselben Zeile. Drag
zwischen Rows um umzuordnen. Klick auf Field oben fügt es in 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 <StempelLayoutBuilder
layout={layout} layout={layout}
availableFields={availableFields} availableFields={availableFields}
onChange={(newLayout) => onUpdate({ layout: newLayout })} /> onChange={(newLayout) => onUpdate({ layout: newLayout })} />
)}
{/* Hinweis: Typografie (Font/Stil/Höhe) wird in der OBERLEISTE {/* Hinweis: Typografie (Font/Stil/Höhe) wird in der OBERLEISTE
gesetzt — den Stempel im Viewport anklicken. Aenderungen werden 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, display: 'flex', alignItems: 'center', gap: 4,
}}> }}>
<Icon name="info" size={11} style={{ color: 'var(--text-muted)' }} /> <Icon name="info" size={11} style={{ color: 'var(--text-muted)' }} />
Typografie (Font/Stil/Höhe): Stempel im Viewport selektieren {isFlaeche
Oberleiste. ? `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>
<div style={{ <div style={{