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:
+8
-1
@@ -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>
|
||||
|
||||
@@ -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
@@ -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) */}
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function ElementePropertiesApp() {
|
||||
geschosse={state.geschosse || []}
|
||||
materials={state.materials || []}
|
||||
hatchPatterns={state.hatchPatterns}
|
||||
oeffStyles={state.oeffStyles || []}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
|
||||
+10
-38
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
{(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user