Fenster/Tueren LoD + Stile + Phase-3-Ausschnitt-Darstellung + UI-Konsistenz

Fenster/Tueren:
- 3-stufige SIA-400-Darstellung pro Element: einfach (1:100, flache
  Scheibe ohne Tiefe in Wand-Mittelebene), standard (1:50, Rahmen +
  Glas + Sims), detail (1:20, Doppelverglasung).
- Aussenseite-Flag mit Auto-Detection aus der Click-Richtung beim
  Setzen — Sim sitzt automatisch aussen. Im Panel als Umkehren-Toggle.
- Tueren-Rahmen-Typ Zarge|Block — Blockrahmen ragt seitlich raus.
- Rahmen-Offset (m von Wand-Innenseite) ersetzt das 3-Preset Lage-
  Feld. Wirkt auch in der einfachen Darstellung (Pane sitzt auf der
  Rahmen-Mittelebene, nicht in Wand-Mitte).
- Sims nur AUSSEN. Innen entfaellt — der Sim ist gleichzeitig der
  visuelle Indikator fuer die Aussenseite.
- Oeffnungs-Stile: list/save/delete-API mit 6 Default-Presets
  (Fenster Standard/Gross/Bandlage, Tuer Innen/Eingang/Verglast).
  Style-ID per UserString am Objekt persistiert. Im Panel BarCombo
  mit "Aktuelle als Stil speichern…". Beim Rhino-Command "Stil"-
  Option zum Picken vor dem Klick.

Ausschnitt-Darstellung (Phase 3):
- Doc-Level Override dossier_aktive_darstellung gewinnt vor per-
  Object-Setting. Wechsel triggert Regen aller Oeffnungen via neuer
  regenerate_all_oeffnungen-API.
- Ausschnitt-Capture speichert die Darstellung mit, Restore wendet
  sie an und regeneriert.
- Oberleiste-Quick-Switch BarCombo mit 4 Optionen.
- AusschnittSettings-Dialog: Darstellungs-Dropdown.

Gestaltung (SectionStyle Phase 2):
- _set_section_style schreibt per-Object SectionHatchIndex/Scale/
  Rotation/Color mit Multi-Fallback (Property-Namen varieren je
  Rhino-Build). _selection_summary liest die selben zurueck.
- HatchEditor als shared Component fuer Fill + Section.
- geometryKind ignoriert DOSSIER-Source-Curves damit Wand-Selektion
  (Axis + Volume) als 3D klassifiziert wird.

UI-Konsistenz Panels:
- Ebenenkombi zurueck als eigene Section oben im Ebenen-Panel,
  Modelldarstellung-Dropdown an die freigewordene Position in der
  Oberleiste (Row 1 Col 2 im 2x2-Preset-Block).
- BarCombo erweitert: stretch-Prop (Pill waechst auf Container-
  Breite), onSecond/secondIcon/secondTitle fuer 2. Trailing-Button,
  gearIcon-Prop. Plus-Slot immer ganz aussen rechts, Settings-Slot
  direkt nach dem Caret.
- Ebenen + Zeichnungsebenen visuell kohaerent: identisches Padding
  (1px 12px 1px 0), Chevron/Spacer-Slot 12px, Master-Row mit Eye
  16x16 + Lock 14x14, gleiche Border + Borderfarbe. Eye-Icons in
  beiden Panels untereinander ausgerichtet.
- Properties-Container ohne Border (war zuvor accent-gruen, dann
  border — User wollte gar nichts mehr).
- ElementList raus aus dem Elemente-Panel (Uebersicht via Tree-
  Window erreichbar). NeuesElement bleibt voll sichtbar bei
  Selektion (kein Collapse), Properties oben.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 12:34:15 +02:00
parent 9ae8574ab0
commit 0c5f8055a5
13 changed files with 899 additions and 220 deletions
+8 -1
View File
@@ -32,11 +32,16 @@ export default function App() {
const [appliedE, setAppliedE] = useState(INITIAL_EBENEN)
const [eMode, setEMode] = useState('all')
const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60'])
const [layerCombinations, setLayerCombinations] = useState([])
const [activeKombi, setActiveKombi] = useState(null)
useEffect(() => {
onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => {
onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp,
layerCombinations: lc, layerCombinationActive: ac }) => {
if (e) { setEbenen(e); setAppliedE(e) }
if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp)
if (Array.isArray(lc)) setLayerCombinations(lc)
if (ac !== undefined) setActiveKombi(ac)
})
onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => {
// Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir
@@ -132,6 +137,8 @@ export default function App() {
mode={eMode}
onModeChange={setEMode}
hatchPatterns={hatchPatterns}
layerCombinations={layerCombinations}
activeKombi={activeKombi}
/>
</div>
</div>
+15
View File
@@ -65,6 +65,7 @@ export default function AusschnittSettingsApp() {
overridesEnabled: !!snap.overridesEnabled,
overridesPreset: snap.overridesPreset || '',
layerCombination: snap.layerCombination || '',
darstellung: snap.darstellung || '',
},
})
}
@@ -88,6 +89,20 @@ export default function AusschnittSettingsApp() {
/>
</Field>
<Field label="DARSTELLUNG"
hint="SIA-400 Detaillierungsgrad — leer = per-Element-Setting respektieren">
<select
value={snap.darstellung || ''}
onChange={(ev) => set({ darstellung: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value=""> per Element </option>
<option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option>
</select>
</Field>
<Field label="BILDSCHIRMMODUS"
hint="Display-Mode des Viewports beim Wiederherstellen">
<select
+123 -49
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import Icon from './components/Icon'
import { BarToggle, BarButton } from './components/BarControls'
import { BarToggle, BarButton, BarCombo } from './components/BarControls'
import {
onMessage, notifyReady,
createWall, createDecke, createDach,
@@ -8,6 +8,7 @@ import {
createStuetze, createTraeger, createRaum,
openSwisstopo, openSwisstopoDialog, openOsmDialog,
updateElement, deleteElement, openElementeUebersicht, openElementeProperties,
saveOeffStyle, deleteOeffStyle,
} from './lib/rhinoBridge'
const labelXs = {
@@ -494,7 +495,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 }) {
export function PropertiesView({ selected, geschosse, materials, hatchPatterns, oeffStyles }) {
if (!selected) return null
const upd = (p) => updateElement(selected.id, p)
const del = (label) => () => { if (window.confirm(`${label} löschen?`)) deleteElement(selected.id) }
@@ -521,6 +522,7 @@ export function PropertiesView({ selected, geschosse, materials, hatchPatterns }
return <AussparungProperties aussp={selected} onDelete={del('Aussparung')} />
// fenster/tuer
return <OeffnungProperties oeff={selected} onUpdate={upd}
oeffStyles={oeffStyles || []}
onDelete={del(selected.kind === 'fenster' ? 'Fenster' : 'Tür')} />
}
@@ -566,7 +568,8 @@ export default function ElementeApp() {
selected={selected}
geschosse={geschosse}
materials={state.materials || []}
hatchPatterns={state.hatchPatterns} />
hatchPatterns={state.hatchPatterns}
oeffStyles={state.oeffStyles || []} />
</div>
)}
<NeuesElementSection
@@ -615,7 +618,6 @@ function TragwerkProperties({ el, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -717,7 +719,6 @@ function RaumProperties({ raum, geschosse, onUpdate, onDelete, hatchPatterns })
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -858,7 +859,6 @@ function AussparungProperties({ aussp, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -910,7 +910,6 @@ function WallProperties({ wall, geschosse, materials, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1123,7 +1122,6 @@ function DeckenProperties({ decke, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1199,7 +1197,6 @@ function DachProperties({ dach, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1517,7 +1514,6 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1684,7 +1680,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
)
}
function OeffnungProperties({ oeff, onUpdate, onDelete }) {
function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) {
const isFenster = oeff.kind === 'fenster'
const label = isFenster ? 'Fenster' : 'Tür'
const icon = isFenster ? 'window' : 'sensor_door'
@@ -1709,16 +1705,13 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
}
const fluegel = oeff.fluegel ?? 1
const rahmenPos = oeff.rahmenPos ?? 'mid'
const simsAus = oeff.simsAus ?? 'ohne'
const simsIn = oeff.simsIn ?? 'ohne'
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
padding: 10, marginBottom: 8,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
@@ -1731,6 +1724,97 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</button>
</div>
{/* Stil-Picker — Liste passender Styles (gefiltert nach typ) */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Stil — gespeicherte Properties-Sets fuer Fenster/Tueren">
Stil
</span>
<div style={{ flex: 1, display: 'flex' }}>
<BarCombo
value={oeff.styleId || ''}
onChange={(v) => {
if (v === '__save__') {
const sugg = (oeffStyles.find(s => s.id === oeff.styleId) || {}).name || ''
const n = (window.prompt('Name fuer neuen Stil:', sugg || (isFenster ? 'Mein Fenster' : 'Meine Tuer')) || '').trim()
if (!n) return
saveOeffStyle(n, {
typ: isFenster ? 'fenster' : 'tuer',
breite: oeff.breite, hoehe: oeff.hoehe, brueest: oeff.brueest,
rahmenB: oeff.rahmenB, rahmenTiefe: oeff.rahmenTiefe,
rahmenOffset: oeff.rahmenOffset,
fluegel: oeff.fluegel, simsAus: oeff.simsAus,
glas: oeff.glas, darstellung: oeff.darstellung,
tuerRahmen: oeff.tuerRahmen,
})
return
}
if (v === '__delete__') {
if (oeff.styleId && window.confirm('Aktiven Stil loeschen?'))
deleteOeffStyle(oeff.styleId)
return
}
onUpdate({ styleId: v })
}}
title="Stil anwenden — alle Properties werden gesetzt">
<option value=""> Eigene Werte </option>
{oeffStyles
.filter(s => s.typ === (isFenster ? 'fenster' : 'tuer'))
.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
<option disabled></option>
<option value="__save__">+ Aktuelle als Stil speichern</option>
{oeff.styleId && <option value="__delete__">🗑 Aktiven Stil loeschen</option>}
</BarCombo>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="SIA-400 Detaillierungsgrad. Einfach=1:100, Standard=1:50, Detail=1:20">
Darstell.
</span>
<div style={{ flex: 1, display: 'flex' }}>
<BarCombo
value={oeff.darstellung || 'standard'}
onChange={(v) => onUpdate({ darstellung: v })}
title="Detaillierungsgrad — beeinflusst die generierte Geometrie">
<option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option>
</BarCombo>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Orientierung — welche Seite der Wand ist aussen. Beim Setzen aus der Click-Richtung erkannt, hier umkehren falls falsch.">
Orient.
</span>
<div style={{ flex: 1, display: 'flex' }}>
<BarToggle label="Umkehren"
onClick={() => onUpdate({ aussenseite:
(oeff.aussenseite || 'rechts') === 'rechts' ? 'links' : 'rechts' })}
title="Aussenseite auf die andere Wandseite umkehren" />
</div>
</div>
{!isFenster && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Tueren-Rahmen-Typ. Zarge sitzt in der Oeffnung, Blockrahmen sitzt aussen herum">
Rahmen
</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
<BarToggle label="Zarge"
active={(oeff.tuerRahmen || 'zarge') === 'zarge'}
onClick={() => onUpdate({ tuerRahmen: 'zarge' })} />
<BarToggle label="Block"
active={(oeff.tuerRahmen || 'zarge') === 'block'}
onClick={() => onUpdate({ tuerRahmen: 'block' })} />
</div>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
<input type="text" value={breite}
@@ -1811,21 +1895,21 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
</div>
{/* Rahmen-Lage im Wandquerschnitt */}
{/* Rahmen-Lage: Abstand der Rahmen-Innenkante von der Wand-Innenseite */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Lage des Rahmens im Wandquerschnitt">
title="Abstand der Rahmen-Innenkante von der Wand-Innenseite (Aussenseite-Flag oben bestimmt welche Seite innen ist)">
Lage
</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
{RAHMEN_POS_OPTIONS.map(o => (
<BarToggle key={o.code}
label={o.label}
active={rahmenPos === o.code}
onClick={() => onUpdate({ rahmenPos: o.code })}
title={o.hint} />
))}
</div>
<input type="text"
value={String(oeff.rahmenOffset ?? 0.05)}
onChange={(e) => {
const v = parseFloat(e.target.value.replace(',', '.'))
if (!Number.isNaN(v) && v >= 0) onUpdate({ rahmenOffset: v })
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m v. innen</span>
</div>
{/* Fluegel-Anzahl — nur fuer Fenster (Tueren haben ein einzelnes Tuerblatt) */}
@@ -1846,34 +1930,24 @@ function OeffnungProperties({ oeff, onUpdate, onDelete }) {
</div>
)}
{/* Sims-Stile (aussen / innen) — nur fuer Fenster */}
{/* Sims — nur aussen. Innen gibt's bewusst nicht. Dient zugleich
als visueller Indikator fuer die Aussenseite-Einstellung. */}
{isFenster && (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Aussensims — Platte unter Öffnung, ragt aussen heraus">
Sims a.
</span>
<select value={simsAus}
onChange={(e) => onUpdate({ simsAus: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Aussensims — Platte unter Öffnung, ragt aussen heraus">
Sims
</span>
<div style={{ flex: 1, display: 'flex' }}>
<BarCombo
value={simsAus}
onChange={(v) => onUpdate({ simsAus: v })}
title="Sims-Stil">
{SIMS_OPTIONS.map(o =>
<option key={o.code} value={o.code}>{o.label}</option>)}
</select>
</BarCombo>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Innensims — Platte unter Öffnung, ragt innen heraus">
Sims i.
</span>
<select value={simsIn}
onChange={(e) => onUpdate({ simsIn: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
{SIMS_OPTIONS.map(o =>
<option key={o.code} value={o.code}>{o.label}</option>)}
</select>
</div>
</>
</div>
)}
{/* Glas-Toggle: bei Tueren ersetzt Glas das Tuerblatt (verglaste Tuer) */}
+1
View File
@@ -33,6 +33,7 @@ export default function ElementePropertiesApp() {
geschosse={state.geschosse || []}
materials={state.materials || []}
hatchPatterns={state.hatchPatterns}
oeffStyles={state.oeffStyles || []}
/>
) : (
<div style={{
+10 -38
View File
@@ -14,6 +14,7 @@ import {
setMasseActive, openMasseSettings,
openAbout, createText, setTextSettings,
applyTextStyle, saveTextStyle, deleteTextStyle,
setDarstellung,
} from './lib/rhinoBridge'
const PRESETS = [
@@ -399,47 +400,18 @@ export default function OberleisteApp() {
<option key={dm.id} value={dm.name}>{dm.name}</option>
))}
</BarCombo>
{/* Reihe 1, Spalte 2: Ebenenkombination */}
{/* Reihe 1, Spalte 2: Modelldarstellung (SIA-400 LoD) */}
<BarCombo
icon="layers"
value={state.layerCombinationActive || '__none__'}
onChange={(v) => {
if (v === '__configure__') { openLayerCombinationsDialog(); return }
if (v === '__save__') {
const suggested = state.layerCombinationActive
|| `Kombi ${(state.layerCombinations || []).length + 1}`
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
if (!name) return
if ((state.layerCombinations || []).includes(name) &&
!window.confirm(`"${name}" überschreiben?`)) return
saveLayerCombination(name)
return
}
if (v === '__delete__') {
if (state.layerCombinationActive &&
window.confirm(`Kombination "${state.layerCombinationActive}" löschen?`))
deleteLayerCombination(state.layerCombinationActive)
return
}
pickLayerCombination(v === '__none__' ? null : v)
}}
title={state.layerCombinationActive
? `Aktive Kombi: ${state.layerCombinationActive}`
: 'Keine Kombination — manuelle Sichtbarkeit'}
icon="tune"
value={state.aktiveDarstellung || ''}
onChange={(v) => setDarstellung(v)}
title="Darstellungs-Override fuer Fenster/Tueren (SIA-400 LoD)"
width={PRESET_W}
onGear={openLayerCombinationsDialog}
gearTitle="Ebenenkombinationen bearbeiten"
>
<option value="__none__"> Eigene </option>
{(state.layerCombinations || []).map(name => (
<option key={name} value={name}>{name}</option>
))}
<option disabled></option>
<option value="__save__">+ Aktuelle speichern</option>
{state.layerCombinationActive && (
<option value="__delete__">🗑 Aktuelle löschen</option>
)}
<option value="__configure__">Bearbeiten</option>
<option value=""> per Element </option>
<option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option>
</BarCombo>
{/* Reihe 2, Spalte 1: Overrides (Toggle als Icon links) */}
<BarCombo
+34 -8
View File
@@ -14,12 +14,18 @@ export const BAR_H = 22
export function BarCombo({
icon, iconActive, iconClickable, onIconClick, iconTitle,
value, onChange, width, title, children, disabled,
onGear, gearTitle, valueAccent,
onGear, gearTitle, gearIcon, valueAccent,
onSecond, secondIcon, secondTitle,
stretch,
}) {
return (
<div style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
opacity: disabled ? 0.5 : 1, flexShrink: 0,
display: stretch ? 'flex' : 'inline-flex',
alignItems: 'center', gap: 5,
opacity: disabled ? 0.5 : 1,
flex: stretch ? 1 : 'none',
flexShrink: 0,
minWidth: 0,
}}>
{icon && (iconClickable ? (
<button onClick={onIconClick} title={iconTitle}
@@ -51,8 +57,12 @@ export function BarCombo({
e.currentTarget.style.background = 'var(--bg-input)'
}}
style={{
display: 'inline-flex', alignItems: 'stretch',
height: BAR_H + 2, width, boxSizing: 'border-box',
display: stretch ? 'flex' : 'inline-flex', alignItems: 'stretch',
height: BAR_H + 2,
width: stretch ? '100%' : width,
flex: stretch ? 1 : 'none',
minWidth: 0,
boxSizing: 'border-box',
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 999,
@@ -74,20 +84,36 @@ export function BarCombo({
appearance: 'none', WebkitAppearance: 'none',
backgroundImage: 'var(--select-arrow)',
backgroundRepeat: 'no-repeat',
backgroundPosition: onGear ? 'right 1px center' : 'right 10px center',
backgroundPosition: onGear ? 'right 6px center' : 'right 10px center',
cursor: disabled ? 'not-allowed' : 'pointer',
letterSpacing: 0,
}}
>{children}</select>
{/* Trailing-Slots in DOM-Reihenfolge (von Select-Caret nach
aussen rechts): zuerst onGear (Settings), dann onSecond (Add).
Konvention: Settings sitzt immer DIREKT nach dem Caret,
"+" sitzt immer GANZ AUSSEN rechts. */}
{onGear && (
<button onClick={onGear} title={gearTitle}
style={{
background: 'transparent', border: 'none',
padding: '0 8px', cursor: 'pointer',
padding: '0 4px', marginLeft: 3, cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<Icon name="settings" size={12}
<Icon name={gearIcon || 'settings'} size={12}
style={{ color: 'var(--text-muted)' }} />
</button>
)}
{onSecond && (
<button onClick={onSecond} title={secondTitle}
style={{
background: 'transparent', border: 'none',
padding: '0 8px 0 4px', marginLeft: 2, cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<Icon name={secondIcon || 'settings'} size={12}
style={{ color: 'var(--text-muted)' }} />
</button>
)}
+86 -39
View File
@@ -3,7 +3,9 @@ import Icon from './Icon'
import ConfirmDeleteEbene from './ConfirmDeleteEbene'
import ContextMenu from './ContextMenu'
import { BarCombo, BarButton } from './BarControls'
import { setLayerStyle, deleteEbene, moveSelectionToEbene, openEbenenSettings } from '../lib/rhinoBridge'
import { setLayerStyle, deleteEbene, moveSelectionToEbene, openEbenenSettings,
pickLayerCombination, saveLayerCombination, deleteLayerCombination,
openLayerCombinationsDialog } from '../lib/rhinoBridge'
const MODES = [
{ value: 'all_force', label: 'Alle anzeigen' },
@@ -245,8 +247,8 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
onContextMenu={onContextMenu}
style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '1px 8px',
paddingLeft: 6 + (depth || 0) * 10,
padding: '1px 12px 1px 0',
paddingLeft: (depth || 0) * 12,
margin: 0,
background: active ? 'var(--active-dim)'
: (e.visible !== false) ? 'var(--bg-item)'
@@ -345,6 +347,7 @@ function SortHeader({ label, sortKey, sortBy, sortDir, onSort, style }) {
export default function EbenenManager({
ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns,
layerCombinations = [], activeKombi = null,
}) {
const [sortBy, setSortBy] = useState('code')
const [sortDir, setSortDir] = useState('asc')
@@ -548,70 +551,114 @@ export default function EbenenManager({
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Sichtbarkeit</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ flex: 1, minWidth: 0, display: 'flex' }}>
<BarCombo
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
>
{MODES.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
</BarCombo>
</div>
<BarButton icon="add" onClick={addNew} title="Ebene hinzufügen" />
<span className="label-xs">Ebenenkombination</span>
<div style={{ display: 'flex', width: '100%' }}>
<BarCombo
stretch
icon="layers"
value={activeKombi || '__none__'}
onChange={(v) => {
if (v === '__configure__') { openLayerCombinationsDialog(); return }
if (v === '__save__') {
const suggested = activeKombi || `Kombi ${layerCombinations.length + 1}`
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
if (!name) return
if (layerCombinations.includes(name) &&
!window.confirm(`"${name}" überschreiben?`)) return
saveLayerCombination(name)
return
}
if (v === '__delete__') {
if (activeKombi &&
window.confirm(`Kombination "${activeKombi}" löschen?`))
deleteLayerCombination(activeKombi)
return
}
pickLayerCombination(v === '__none__' ? null : v)
}}
title={activeKombi
? `Aktive Kombi: ${activeKombi}`
: 'Keine Kombination — manuelle Sichtbarkeit'}
onGear={openLayerCombinationsDialog}
gearTitle="Ebenenkombinationen bearbeiten"
>
<option value="__none__"> Eigene </option>
{layerCombinations.map(n => (
<option key={n} value={n}>{n}</option>
))}
<option disabled></option>
<option value="__save__">+ Aktuelle speichern</option>
{activeKombi && (
<option value="__delete__">🗑 Aktuelle löschen</option>
)}
<option value="__configure__">Bearbeiten</option>
</BarCombo>
</div>
</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '2px 8px 2px 9px',
display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 14px',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border)',
borderBottom: '1px solid var(--border-light)',
}}>
{/* Master-Eye: alle Ebenen sichtbar/unsichtbar */}
<span className="label-xs">Sichtbarkeit</span>
<div style={{ display: 'flex', width: '100%' }}>
<BarCombo
stretch
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
onSecond={addNew}
secondIcon="add"
secondTitle="Ebene hinzufügen"
>
{MODES.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
</BarCombo>
</div>
</div>
{/* Sort-Header-Row + Master-Eye/Lock. Padding-Left identisch zu
den Data-Rows damit Eye-Icons aligned sind. Erste 12px-Spanne
spiegelt den Expand-Chevron-Slot der Data-Rows wider. */}
<div style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '2px 12px 2px 0',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span style={{ width: 12, flexShrink: 0 }} />
<button
className="btn-icon-xs"
onClick={() => {
const anyVisible = ebenen.some(e => e.visible !== false)
// Wenn irgendeine sichtbar -> alle aus. Wenn keine sichtbar -> alle an.
onChange(ebenen.map(e => ({ ...e, visible: !anyVisible })))
if (mode === 'active' || mode === 'all_force') onModeChange('all')
}}
title={ebenen.every(e => e.visible !== false)
? 'Alle Ebenen ausblenden'
: 'Alle Ebenen einblenden'}
style={{ width: 18, height: 18,
? 'Alle Ebenen ausblenden' : 'Alle Ebenen einblenden'}
style={{ width: 16, height: 16,
opacity: (mode === 'active' || mode === 'all_force') ? 0.5 : 1 }}
>
<Icon
name={ebenen.every(e => e.visible !== false) ? 'visibility' : 'visibility_off'}
size={12}
/>
<Icon name={ebenen.every(e => e.visible !== false) ? 'visibility' : 'visibility_off'} size={11} />
</button>
<SortHeader label="Cd" sortKey="code" sortBy={sortBy} sortDir={sortDir} onSort={toggleSort} style={{ width: 24 }} />
<SortHeader label="N" sortKey="code" sortBy={sortBy} sortDir={sortDir} onSort={toggleSort} style={{ width: 22 }} />
<div style={{ width: 12 }} />
<SortHeader label="Name" sortKey="name" sortBy={sortBy} sortDir={sortDir} onSort={toggleSort} style={{ flex: 1 }} />
<SortHeader label="Lw" sortKey="lw" sortBy={sortBy} sortDir={sortDir} onSort={toggleSort} style={{ width: 42, textAlign: 'right', display: 'block' }} />
{/* Master-Lock: alle Ebenen sperren/entsperren */}
<button
className="btn-icon-xs"
onClick={() => {
const anyLocked = ebenen.some(e => e.locked === true)
onChange(ebenen.map(e => ({ ...e, locked: !anyLocked })))
}}
title={ebenen.every(e => e.locked === true)
? 'Alle Ebenen entsperren'
: 'Alle Ebenen sperren'}
style={{ width: 18, height: 18 }}
title={ebenen.every(e => e.locked === true) ? 'Alle Ebenen entsperren' : 'Alle Ebenen sperren'}
style={{ width: 14, height: 14 }}
>
<Icon
name={ebenen.every(e => e.locked === true) ? 'lock' : 'lock_open'}
size={11}
/>
<Icon name={ebenen.every(e => e.locked === true) ? 'lock' : 'lock_open'} size={11} />
</button>
<div style={{ width: 18 }} />
<div style={{ width: 14 }} />
</div>
{(() => {
+33 -25
View File
@@ -48,7 +48,7 @@ function ZeichnungsebeneRow({
onContextMenu={onContextMenu}
style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '1px 12px',
padding: '1px 12px 1px 0',
margin: 0,
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
borderRadius: active ? 999 : 0,
@@ -58,6 +58,9 @@ function ZeichnungsebeneRow({
minHeight: 24,
}}
>
{/* Spacer-Slot — spiegelt den Chevron-Slot bei Ebenen-Rows wider
damit die Eye-Icons beider Panels untereinander stehen. */}
<span style={{ width: 12, flexShrink: 0 }} />
<button
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }}
@@ -213,32 +216,37 @@ export default function GeschossManager({
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Sichtbarkeit</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ flex: 1, minWidth: 0, display: 'flex' }}>
<BarCombo
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
>
{MODES.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</BarCombo>
</div>
<BarButton icon="add" onClick={addQuick} title="Zeichnungsebene hinzufügen" />
<BarButton icon="settings" onClick={() => openGeschossDialog(zeichnungsebenen)} title="Einstellungen" />
<div style={{ display: 'flex', width: '100%' }}>
<BarCombo
stretch
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
onGear={() => openGeschossDialog(zeichnungsebenen)}
gearIcon="settings"
gearTitle="Einstellungen"
onSecond={addQuick}
secondIcon="add"
secondTitle="Zeichnungsebene hinzufügen"
>
{MODES.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</BarCombo>
</div>
</div>
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
EbenenManager). */}
{/* Master-Row analog EbenenManager. Padding + Icon-Sizes identisch
damit beide Panels visuell kohaerent sind. Erste 12px-Spanne
spiegelt den Chevron-Slot der Ebenen-Daten-Rows wider. */}
<div style={{
display: 'flex', alignItems: 'center', gap: 5,
padding: '2px 14px',
display: 'flex', alignItems: 'center', gap: 4,
padding: '2px 12px 2px 0',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border)',
borderBottom: '1px solid var(--border-light)',
}}>
<span style={{ width: 12, flexShrink: 0 }} />
<button
className="btn-icon-xs"
onClick={() => {
@@ -249,12 +257,12 @@ export default function GeschossManager({
title={zeichnungsebenen.every(z => z.visible !== false)
? 'Alle Zeichnungsebenen ausblenden'
: 'Alle Zeichnungsebenen einblenden'}
style={{ width: 18, height: 18,
style={{ width: 16, height: 16,
opacity: (mode === 'active' || mode === 'all_force') ? 0.5 : 1 }}
>
<Icon
name={zeichnungsebenen.every(z => z.visible !== false) ? 'visibility' : 'visibility_off'}
size={12}
size={11}
/>
</button>
<span style={{ flex: 1 }} />
@@ -267,14 +275,14 @@ export default function GeschossManager({
title={zeichnungsebenen.every(z => z.locked === true)
? 'Alle Zeichnungsebenen entsperren'
: 'Alle Zeichnungsebenen sperren'}
style={{ width: 18, height: 18 }}
style={{ width: 14, height: 14 }}
>
<Icon
name={zeichnungsebenen.every(z => z.locked === true) ? 'lock' : 'lock_open'}
size={11}
/>
</button>
<div style={{ width: 18 }} />
<div style={{ width: 14 }} />
</div>
<div>
+5
View File
@@ -172,6 +172,11 @@ export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) }
export function openElementeUebersicht() { send('OPEN_ELEMENTE_UEBERSICHT', {}) }
export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {}) }
export function setDarstellung(d) { send('SET_DARSTELLUNG', { darstellung: d || '' }) }
export function saveOeffStyle(name, settings) {
send('SAVE_OEFF_STYLE', { name, settings })
}
export function deleteOeffStyle(id) { send('DELETE_OEFF_STYLE', { id }) }
export function setSectionStyle(enabled, source, color, pattern, scale, rotation) {
send('SET_SECTION_STYLE', { enabled, source, color, pattern, scale, rotation })
}