Stempel-Element: SIA-Bilanz als platzierbares Viewport-Objekt

Neuer Element-Type "stempel" — TextEntity die automatisch eine SIA-416
Bilanz aggregiert und im Plan platziert wird. Re-rendert sich live wenn
sich Raeume im Scope aendern.

Backend (elemente.py):
- Neue SIA-Tags: GF (Geschossflaeche), AGF (Aussengeschossflaeche)
  mit eigenen Labels + Pastell-Farben in _SIA_COLORS_HEX
- "stempel" als SOURCE_TYPE; eigene UserStrings:
  - stempel_scope: "total" | "geschoss:<gid>"
  - stempel_txt_h, stempel_font, stempel_bold, stempel_italic
- compute_sia_bilanz(doc, scope): aggregiert nach SIA-Tags, liefert
  HNF/NNF/VF/FF/GF/AGF + abgeleitet NF/NGF/count + Scope-Label
- _format_bilanz_lines: kompakte Stempel-Textzeilen ("HNF  120.5 m²"),
  Trennlinien + nur Kategorien > 0
- _make_stempel_text: TextEntity-Builder mit Header "Nutzflächen · {Scope}"
- _regenerate_element_body "stempel"-Branch: in-place Replace mit
  aktualisiertem Text (Position bleibt aus alter Geometrie)
- _regenerate_stempel_for_geschoss: regennt alle Stempel im selben
  Geschoss + alle "total"-Stempel
- Auto-Cascade: raum_outline-Regen setzt sticky-marker; nach REGEN_BUSY-
  Release wird _regenerate_stempel_for_geschoss aufgerufen
- _cmd_create_stempel: GetPoint im Viewport, layer = aktives Geschoss
  Raum-Sublayer, default-Scope = aktives Geschoss
- _update_wall "stempel"-Branch: scope/txtH/font/bold/italic via patch
- elemente_uebersicht: SIA-Bilanz um gf/agf/ngf erweitert; "stempel"
  als KIND-Eintrag

Frontend:
- createStempel-Bridge-Export
- "Stempel"-Pill-Button in der "Raeume"-PillGroup
- StempelProperties-Component: Scope-Dropdown (Total + alle Geschosse),
  Bilanz-Vorschau mit Hervorhebung der NF (Accent-Farbe)
- KIND_META + RAUM_SIA_KINDS um GF/AGF erweitert

Workflow: Pill "Stempel" klicken → Punkt im Viewport → Stempel erscheint
mit Header "Nutzflächen · EG" + Bilanz. Properties: Scope auf Total
umstellen oder anderes Geschoss waehlen. Neue Raeume taggen mit SIA-
Tag → Stempel aktualisiert sich automatisch.
This commit is contained in:
2026-05-27 00:10:02 +02:00
parent 7fbda8c289
commit 2386366566
5 changed files with 440 additions and 9 deletions
+103 -1
View File
@@ -7,7 +7,7 @@ import {
onMessage, notifyReady,
createWall, createDecke, createDach,
createFenster, createTuer, createAussparung, createTreppe,
createStuetze, createTraeger, createRaum,
createStuetze, createTraeger, createRaum, createStempel,
openSwisstopo, openSwisstopoDialog, openOsmDialog,
updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
saveOeffStyle, deleteOeffStyle,
@@ -178,6 +178,7 @@ const KIND_META = {
stuetze: { icon: 'square_foot', label: 'Stütze', color: '#5fa896' },
traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#7fc8a8' },
raum: { icon: 'crop_free', label: 'Raum', color: '#a0a8b0' },
stempel: { icon: 'receipt_long', label: 'Stempel', color: '#5fa896' },
aussparung: { icon: 'rectangle', label: 'Aussparung', color: '#9090a0' },
}
@@ -193,6 +194,8 @@ const RAUM_SIA_KINDS = [
{ code: 'nnf', label: 'NNF', color: '#e8c498', hint: 'Nebennutzfläche' },
{ code: 'vf', label: 'VF', color: '#e8d878', hint: 'Verkehrsfläche' },
{ code: 'ff', label: 'FF', color: '#a8c8e0', hint: 'Funktionsfläche' },
{ code: 'gf', label: 'GF', color: '#d0d0d0', hint: 'Geschossfläche (gross — umfasst das ganze Geschoss)' },
{ code: 'agf', label: 'AGF', color: '#c0d8c0', hint: 'Aussengeschossfläche (Balkone, Terrassen)' },
]
const PROFIL_META = {
@@ -479,6 +482,11 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
'Outline zeichnen · Stempel zeigt Name + Fläche'}
disabled={dis}
onClick={() => createRaum({})} />
<PillButton icon="receipt_long" label="Stempel"
hint={dis ? baseHint('Stempel') :
'SIA-Bilanz-Stempel platzieren · Default = aktives Geschoss · Properties: Total/Geschoss umstellen'}
disabled={dis}
onClick={() => createStempel({})} />
</PillGroup>
<PillGroup label="Library">
@@ -529,6 +537,9 @@ export function PropertiesView({ selected, geschosse, materials, hatchPatterns,
hatchPatterns={hatchPatterns} fonts={fonts || []}
raumStempelStile={raumStempelStile || []}
onUpdate={upd} onDelete={del('Raum')} />
if (selected.kind === 'stempel')
return <StempelProperties stempel={selected} geschosse={geschosse}
onUpdate={upd} onDelete={del('Stempel')} />
if (selected.kind === 'aussparung')
return <AussparungProperties aussp={selected} onDelete={del('Aussparung')} />
@@ -1139,6 +1150,97 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns, fo
)
}
function StempelProperties({ stempel, geschosse, onUpdate, onDelete }) {
const scope = stempel.scope || 'total'
const bilanz = stempel.bilanz || {}
const rows = [
{ key: 'hnf', label: 'HNF', val: bilanz.hnf },
{ key: 'nnf', label: 'NNF', val: bilanz.nnf },
{ key: 'nf', label: 'NF', val: bilanz.nf, accent: true,
hint: 'Nutzfläche = HNF + NNF' },
{ key: 'vf', label: 'VF', val: bilanz.vf },
{ key: 'ff', label: 'FF', val: bilanz.ff },
{ key: 'ngf', label: 'NGF', val: bilanz.ngf,
hint: 'Nettogeschossfläche = NF + VF + FF' },
{ key: 'gf', label: 'GF', val: bilanz.gf },
{ key: 'agf', label: 'AGF', val: bilanz.agf },
].filter(r => r.val && r.val > 0)
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Icon name="receipt_long" size={13}
style={{ color: 'var(--accent)', marginRight: 6 }} />
<span style={{ ...labelXs, flex: 1, color: 'var(--accent)' }}>
Stempel · {stempel.scopeLabel || '—'}
</span>
<button className="btn-icon-sm btn-icon-danger" onClick={onDelete}
title="Löschen">
<Icon name="delete" size={12} />
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 60 }}>Scope</span>
<select value={scope}
onChange={(e) => onUpdate({ scope: e.target.value })}
title="Welche Räume werden im Stempel aggregiert"
style={{ flex: 1, fontSize: 11 }}>
<option value="total">Total · alle Geschosse</option>
{geschosse.filter(g => g.id !== '__keingeschoss__').map(g => (
<option key={g.id} value={`geschoss:${g.id}`}>
Geschoss · {g.name}
</option>
))}
</select>
</div>
<div style={{
paddingTop: 6, borderTop: '1px dashed var(--border)',
fontSize: 10, fontFamily: 'DM Mono, monospace',
color: 'var(--text-muted)',
}}>
<div style={{ fontSize: 9, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase',
marginBottom: 4 }}>
Bilanz · {bilanz.count || 0} Räume klassifiziert
</div>
{rows.length === 0 ? (
<div style={{ fontSize: 10, color: 'var(--text-muted)',
fontStyle: 'italic', padding: '4px 0' }}>
Keine klassifizierten Räume im Scope.
Räume mit SIA-Tag (HNF/NNF/VF/FF/GF/AGF) erscheinen hier.
</div>
) : rows.map(r => (
<div key={r.key} title={r.hint || ''}
style={{ display: 'flex', justifyContent: 'space-between',
padding: '2px 0',
color: r.accent ? 'var(--accent)' : 'inherit',
fontWeight: r.accent ? 600 : 400 }}>
<span>{r.label}</span>
<span>{r.val.toFixed(1)} </span>
</div>
))}
</div>
<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>
)
}
function AussparungProperties({ aussp, onDelete }) {
return (
<div style={{
+2 -1
View File
@@ -10,7 +10,7 @@ import { onMessage, notifyReady, send } from './lib/rhinoBridge'
const KIND_ORDER = [
'wand', 'decke', 'dach', 'fenster', 'tuer', 'aussparung',
'treppe', 'stuetze', 'traeger', 'raum',
'treppe', 'stuetze', 'traeger', 'raum', 'stempel',
]
const KIND_META = {
@@ -24,6 +24,7 @@ const KIND_META = {
stuetze: { icon: 'square_foot', label: 'Stützen', color: '#c87050' },
traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#a87858' },
raum: { icon: 'crop_free', label: 'Räume', color: '#5fa896' },
stempel: { icon: 'receipt_long', label: 'Stempel', color: '#5fa896' },
}
+1
View File
@@ -379,6 +379,7 @@ export function createTreppe(p) { send('CREATE_TREPPE', p || {}) }
export function createStuetze(p) { send('CREATE_STUETZE', p || {}) }
export function createTraeger(p) { send('CREATE_TRAEGER', p || {}) }
export function createRaum(p) { send('CREATE_RAUM', p || {}) }
export function createStempel(p) { send('CREATE_STEMPEL', p || {}) }
export function exportRaeume() { send('EXPORT_RAEUME', {}) }
// Library-Symbol/Object — Picker im Elemente-Panel
export function listLibrary() { send('LIST_LIBRARY', {}) }