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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user