Initial commit — Dossier Rhino 8 Plugin
OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac): - Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde), Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel), Treppen (gerade · L · Wendel mit Schrittmass-Validierung) - Live-Previews mit Step-Lines + Soll-Range-Clamping - Bidirektionale Selection-Sync zwischen Source-Linie und Volume - Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz - Layouts mit PDF-Export - Ausschnitte / Massstab / Override-Regeln - Petrol-Gruen Theme (Rapport-konform) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
import { useState, useRef, useMemo, useEffect } from 'react'
|
||||
import Section from './Section'
|
||||
import Icon from './Icon'
|
||||
import ConfirmDeleteEbene from './ConfirmDeleteEbene'
|
||||
import ContextMenu from './ContextMenu'
|
||||
import EbenenSettingsDialog from './EbenenSettingsDialog'
|
||||
import { setLayerStyle, deleteEbene, moveSelectionToEbene } from '../lib/rhinoBridge'
|
||||
|
||||
const MODES = [
|
||||
{ value: 'all', label: 'Alle anzeigen' },
|
||||
{ value: 'active', label: 'Nur aktive' },
|
||||
{ value: 'grey', label: 'Andere grau' },
|
||||
{ value: 'grey_locked', label: 'Andere grau & gesperrt' },
|
||||
]
|
||||
|
||||
const LW_PRESETS = [0.02, 0.10, 0.13, 0.18, 0.25, 0.35, 0.50, 0.70, 1.00]
|
||||
|
||||
function ColorPicker({ color, onChange }) {
|
||||
const ref = useRef(null)
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={(ev) => { ev.stopPropagation(); ref.current?.click() }}
|
||||
title="Farbe ändern"
|
||||
style={{
|
||||
width: 12, height: 12, borderRadius: 2,
|
||||
background: color, flexShrink: 0,
|
||||
border: '1px solid var(--border)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={ref}
|
||||
type="color"
|
||||
value={color}
|
||||
onChange={(ev) => onChange(ev.target.value)}
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
style={{ position: 'absolute', opacity: 0, width: 0, height: 0, pointerEvents: 'none' }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function LwCell({ lw, onChange }) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [value, setValue] = useState(lw.toFixed(2))
|
||||
|
||||
const commit = () => {
|
||||
const v = parseFloat(value)
|
||||
if (!isNaN(v) && v > 0) onChange(v)
|
||||
else setValue(lw.toFixed(2))
|
||||
setEditing(false)
|
||||
}
|
||||
|
||||
const step = (dir) => {
|
||||
let next
|
||||
if (dir > 0) next = LW_PRESETS.find(p => p > lw + 0.001)
|
||||
else next = [...LW_PRESETS].reverse().find(p => p < lw - 0.001)
|
||||
if (next !== undefined) onChange(next)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 2, width: 42, justifyContent: 'flex-end', flexShrink: 0 }}>
|
||||
{editing ? (
|
||||
<input
|
||||
type="number" step="0.01" min="0.01" max="2.0"
|
||||
value={value}
|
||||
autoFocus
|
||||
onChange={(ev) => setValue(ev.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(ev) => { if (ev.key === 'Enter') commit(); if (ev.key === 'Escape') { setValue(lw.toFixed(2)); setEditing(false) } }}
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
style={{ width: 28, fontSize: 9, padding: '1px 2px', textAlign: 'right' }}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
onDoubleClick={(ev) => { ev.stopPropagation(); setEditing(true); setValue(lw.toFixed(2)) }}
|
||||
title="Doppelklick zum Bearbeiten"
|
||||
style={{ fontSize: 9, fontFamily: 'var(--font-mono)', color: 'var(--text-muted)', cursor: 'text', minWidth: 22, textAlign: 'right' }}
|
||||
>{lw.toFixed(2)}</span>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
<button className="btn-step" onClick={(ev) => { ev.stopPropagation(); step(+1) }} style={{ width: 10, height: 7 }}>
|
||||
<Icon name="arrow_drop_up" size={10} />
|
||||
</button>
|
||||
<button className="btn-step" onClick={(ev) => { ev.stopPropagation(); step(-1) }} style={{ width: 10, height: 7 }}>
|
||||
<Icon name="arrow_drop_down" size={10} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EditableText({ value, onCommit, style, fontWeight, fontSize, autoEditTrigger }) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [val, setVal] = useState(value)
|
||||
const lastTrigger = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
// External trigger -> Edit-Modus aktivieren
|
||||
useEffect(() => {
|
||||
if (autoEditTrigger && autoEditTrigger !== lastTrigger.current) {
|
||||
lastTrigger.current = autoEditTrigger
|
||||
setVal(value)
|
||||
setEditing(true)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoEditTrigger])
|
||||
|
||||
// Beim Edit-Start: Inhalt selektieren
|
||||
useEffect(() => {
|
||||
if (editing && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
try { inputRef.current.select() } catch (e) {}
|
||||
}
|
||||
}, [editing])
|
||||
|
||||
const commit = () => {
|
||||
const trimmed = (val ?? '').trim()
|
||||
if (trimmed && trimmed !== value) onCommit(trimmed)
|
||||
else setVal(value)
|
||||
setEditing(false)
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={val}
|
||||
onChange={(ev) => setVal(ev.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') commit()
|
||||
if (ev.key === 'Escape') { setVal(value); setEditing(false) }
|
||||
}}
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
style={{ ...style, fontWeight, fontSize, padding: '1px 4px' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span
|
||||
onDoubleClick={(ev) => { ev.stopPropagation(); setVal(value); setEditing(true) }}
|
||||
title="Doppelklick zum Umbenennen"
|
||||
style={{ ...style, fontWeight, fontSize, cursor: 'text' }}
|
||||
>{value}</span>
|
||||
)
|
||||
}
|
||||
|
||||
function EbeneRow({ e, active, mode, onClick, onContextMenu, onToggleVisible, onToggleLock, onColorChange, onLwChange, onNameChange, onCodeChange, onDelete, autoEditCode, autoEditName, rowRef }) {
|
||||
// Auge zeigt den Eye-State (User-Intention) — auch fuer die aktive Ebene.
|
||||
// So sieht man auf einen Blick ob sie "normalerweise" sichtbar waere.
|
||||
// Aktive Ebene rendert Rhino zwar immer sichtbar, das visible-Flag bleibt
|
||||
// aber die gespeicherte Intention bis sie wieder de-aktiviert wird.
|
||||
const eyeShown = mode !== 'active'
|
||||
return (
|
||||
<div
|
||||
ref={rowRef}
|
||||
data-ebene-code={e.code}
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 5,
|
||||
padding: '3px 12px',
|
||||
margin: active ? '1px 6px' : '0',
|
||||
background: active ? 'var(--active-dim)'
|
||||
: (e.visible !== false) ? 'var(--bg-item)'
|
||||
: 'var(--bg-panel)',
|
||||
// Pill-Form fuer die aktive Ebene, sonst Standard-Zeile mit Bottom-Border
|
||||
borderRadius: active ? 999 : 0,
|
||||
borderLeft: active ? 'none' : '3px solid transparent',
|
||||
borderBottom: active ? 'none' : '1px solid var(--border-light)',
|
||||
boxShadow: active ? 'inset 0 0 0 1px var(--active-light)' : 'none',
|
||||
opacity: (!active && e.visible === false && mode !== 'all') ? 0.45 : 1,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{eyeShown ? (
|
||||
<button
|
||||
className={`btn-icon-sm ${e.visible !== false ? 'is-on' : ''}`}
|
||||
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }}
|
||||
title={
|
||||
active
|
||||
? (e.visible !== false
|
||||
? 'Normalerweise sichtbar (aktive Ebene wird trotzdem gezeigt)'
|
||||
: 'Normalerweise ausgeblendet — wird nur sichtbar weil aktiv')
|
||||
: (e.visible !== false ? 'Ausblenden' : 'Einblenden')
|
||||
}
|
||||
><Icon name={e.visible !== false ? 'visibility' : 'visibility_off'} size={14} /></button>
|
||||
) : (
|
||||
<span style={{ width: 18, flexShrink: 0 }} />
|
||||
)}
|
||||
|
||||
<EditableText
|
||||
value={e.code}
|
||||
onCommit={onCodeChange}
|
||||
autoEditTrigger={autoEditCode}
|
||||
fontSize={9}
|
||||
style={{ fontFamily: 'var(--font-mono)', color: 'var(--text-muted)', width: 24, textAlign: 'left', flexShrink: 0 }}
|
||||
/>
|
||||
|
||||
<ColorPicker color={e.color} onChange={onColorChange} />
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<EditableText
|
||||
value={e.name}
|
||||
onCommit={onNameChange}
|
||||
autoEditTrigger={autoEditName}
|
||||
fontWeight={active ? 700 : 400}
|
||||
fontSize={11}
|
||||
style={{
|
||||
color: active ? 'var(--active-light)'
|
||||
: (e.visible !== false) ? 'var(--text-label)'
|
||||
: 'var(--text-muted)',
|
||||
display: 'inline-block', width: '100%',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<LwCell lw={e.lw} onChange={onLwChange} />
|
||||
|
||||
<button
|
||||
className="btn-icon-xs"
|
||||
onClick={(ev) => { ev.stopPropagation(); onToggleLock() }}
|
||||
title={e.locked ? 'Entsperren' : 'Sperren'}
|
||||
style={{ color: e.locked ? 'var(--warn)' : undefined }}
|
||||
><Icon name={e.locked ? 'lock' : 'lock_open'} size={12} /></button>
|
||||
|
||||
<button
|
||||
className="btn-icon-xs"
|
||||
onClick={(ev) => { ev.stopPropagation(); onDelete() }}
|
||||
title="Löschen"
|
||||
><Icon name="close" size={12} /></button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SortHeader({ label, sortKey, sortBy, sortDir, onSort, style }) {
|
||||
const active = sortBy === sortKey
|
||||
return (
|
||||
<span
|
||||
className="label-xs"
|
||||
onClick={() => onSort(sortKey)}
|
||||
style={{
|
||||
cursor: 'pointer', userSelect: 'none',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 2,
|
||||
color: active ? 'var(--text-secondary)' : undefined, ...style,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{active && <Icon name={sortDir === 'asc' ? 'arrow_upward' : 'arrow_downward'} size={11} />}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default function EbenenManager({
|
||||
ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns,
|
||||
combinations = [], activeCombName = null,
|
||||
onPickCombination, onSaveCurrentCombination, onDeleteCombination,
|
||||
onEditCombinations, onUserVisibilityChange,
|
||||
}) {
|
||||
const [sortBy, setSortBy] = useState('code')
|
||||
const [sortDir, setSortDir] = useState('asc')
|
||||
const [deleteTarget, setDeleteTarget] = useState(null)
|
||||
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, code }
|
||||
const [clipboard, setClipboard] = useState(null) // { color, lw }
|
||||
const [autoEdit, setAutoEdit] = useState(null) // { code, field, token }
|
||||
const [settingsCode, setSettingsCode] = useState(null) // code der Ebene deren Einstellungen offen sind
|
||||
|
||||
// Bei Auto-Edit: in View scrollen
|
||||
useEffect(() => {
|
||||
if (!autoEdit) return
|
||||
const el = document.querySelector(`[data-ebene-code="${autoEdit.code}"]`)
|
||||
if (el && el.scrollIntoView) {
|
||||
el.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
}
|
||||
}, [autoEdit])
|
||||
|
||||
const toggleSort = (key) => {
|
||||
if (sortBy === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc')
|
||||
else { setSortBy(key); setSortDir('asc') }
|
||||
}
|
||||
|
||||
const sortedEbenen = useMemo(() => {
|
||||
const arr = [...ebenen]
|
||||
arr.sort((a, b) => {
|
||||
let cmp = 0
|
||||
if (sortBy === 'code') {
|
||||
const ca = parseInt(a.code, 10), cb = parseInt(b.code, 10)
|
||||
cmp = (!isNaN(ca) && !isNaN(cb)) ? ca - cb : a.code.localeCompare(b.code)
|
||||
} else if (sortBy === 'name') {
|
||||
cmp = a.name.localeCompare(b.name)
|
||||
} else if (sortBy === 'lw') {
|
||||
cmp = a.lw - b.lw
|
||||
}
|
||||
return sortDir === 'desc' ? -cmp : cmp
|
||||
})
|
||||
return arr
|
||||
}, [ebenen, sortBy, sortDir])
|
||||
|
||||
const updateByCode = (code, patch) => {
|
||||
onChange(ebenen.map(e => e.code === code ? { ...e, ...patch } : e))
|
||||
}
|
||||
|
||||
const handleToggleVisible = (code) => {
|
||||
const cur = ebenen.find(e => e.code === code)
|
||||
if (cur) updateByCode(code, { visible: !(cur.visible !== false) })
|
||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
||||
}
|
||||
const handleToggleLock = (code) => {
|
||||
const cur = ebenen.find(e => e.code === code)
|
||||
if (cur) updateByCode(code, { locked: !cur.locked })
|
||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
||||
}
|
||||
const handleColorChange = (code, color) => {
|
||||
updateByCode(code, { color })
|
||||
setLayerStyle(code, color, undefined)
|
||||
}
|
||||
const handleLwChange = (code, lw) => {
|
||||
updateByCode(code, { lw })
|
||||
setLayerStyle(code, undefined, lw)
|
||||
}
|
||||
const handleNameChange = (code, name) => {
|
||||
updateByCode(code, { name })
|
||||
// Wenn wir gerade neue Ebene anlegen (Phase 'name') — fertig
|
||||
if (autoEdit && autoEdit.code === code && autoEdit.field === 'name') {
|
||||
setAutoEdit(null)
|
||||
}
|
||||
}
|
||||
const handleCodeChange = (oldCode, newCode) => {
|
||||
if (ebenen.some(e => e.code === newCode && e.code !== oldCode)) return
|
||||
onChange(ebenen.map(e => e.code === oldCode ? { ...e, code: newCode } : e))
|
||||
// Phase weiterschalten: Code -> Name
|
||||
if (autoEdit && autoEdit.code === oldCode && autoEdit.field === 'code') {
|
||||
setAutoEdit({ code: newCode, field: 'name', token: Date.now() })
|
||||
}
|
||||
}
|
||||
const handleDelete = (code) => {
|
||||
if (ebenen.length <= 1) return
|
||||
setDeleteTarget(code)
|
||||
}
|
||||
|
||||
const confirmDelete = (moveToCode) => {
|
||||
const code = deleteTarget
|
||||
deleteEbene(code, moveToCode)
|
||||
onChange(ebenen.filter(e => e.code !== code))
|
||||
if (activeCode === code) {
|
||||
const next = ebenen.find(e => e.code !== code)
|
||||
if (next) onActiveChange(next.code)
|
||||
}
|
||||
setDeleteTarget(null)
|
||||
}
|
||||
|
||||
const nextFreeCode = () => {
|
||||
const codes = ebenen.map(e => parseInt(e.code, 10)).filter(n => !isNaN(n))
|
||||
const next = codes.length ? Math.max(...codes) + 1 : 50
|
||||
return String(next).padStart(2, '0')
|
||||
}
|
||||
|
||||
const addNew = () => {
|
||||
const code = nextFreeCode()
|
||||
onChange([...ebenen, {
|
||||
code, name: 'NEU',
|
||||
color: '#888888', lw: 0.18, visible: true, locked: false,
|
||||
}])
|
||||
// Code-Feld der neuen Ebene fokussieren
|
||||
setAutoEdit({ code, field: 'code', token: Date.now() })
|
||||
}
|
||||
|
||||
const duplicateEbene = (code) => {
|
||||
const src = ebenen.find(e => e.code === code)
|
||||
if (!src) return
|
||||
onChange([...ebenen, {
|
||||
...src, code: nextFreeCode(), name: src.name + ' KOPIE',
|
||||
}])
|
||||
}
|
||||
|
||||
const copyProps = (code) => {
|
||||
const e = ebenen.find(x => x.code === code)
|
||||
if (e) setClipboard({ color: e.color, lw: e.lw })
|
||||
}
|
||||
|
||||
const pasteProps = (code) => {
|
||||
if (!clipboard) return
|
||||
onChange(ebenen.map(e => e.code === code ? { ...e, ...clipboard } : e))
|
||||
setLayerStyle(code, clipboard.color, clipboard.lw)
|
||||
}
|
||||
|
||||
const openContextMenu = (ev, code) => {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
setCtxMenu({ x: ev.clientX, y: ev.clientY, code })
|
||||
}
|
||||
|
||||
const ctxItems = (code) => [
|
||||
{ label: 'Ebeneneinstellungen…', icon: 'settings', onClick: () => setSettingsCode(code) },
|
||||
{ divider: true },
|
||||
{ label: 'Selektion hierher übertragen', icon: 'move_down', onClick: () => moveSelectionToEbene(code) },
|
||||
{ divider: true },
|
||||
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateEbene(code) },
|
||||
{ label: 'Eigenschaften kopieren', icon: 'colorize', onClick: () => copyProps(code) },
|
||||
{ label: 'Eigenschaften einfügen', icon: 'format_paint', onClick: () => pasteProps(code), disabled: !clipboard },
|
||||
{ divider: true },
|
||||
{ label: 'Löschen', icon: 'delete', onClick: () => handleDelete(code), danger: true,
|
||||
disabled: ebenen.length <= 1 },
|
||||
]
|
||||
|
||||
const actionBtns = (
|
||||
<button className="btn-add" onClick={addNew} title="Ebene hinzufügen">
|
||||
<Icon name="add" size={16} />
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<Section title="Ebenen" badge={ebenen.length} action={actionBtns}>
|
||||
{/* Ebenenkombinationen — Label + Dropdown + Save-As-Plus */}
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 4,
|
||||
padding: '6px 14px',
|
||||
background: 'var(--bg-section)',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<span className="label-xs">Ebenenkombination</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<select
|
||||
value={activeCombName || '__custom__'}
|
||||
onChange={(ev) => {
|
||||
const v = ev.target.value
|
||||
if (v === '__custom__') return
|
||||
if (v === '__delete__') {
|
||||
if (activeCombName && onDeleteCombination) onDeleteCombination(activeCombName)
|
||||
return
|
||||
}
|
||||
if (onPickCombination) onPickCombination(v)
|
||||
}}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
title={activeCombName ? `Aktiv: ${activeCombName}` : 'Eigene Sichtbarkeit (keine Kombination)'}
|
||||
>
|
||||
<option value="__custom__">{activeCombName ? activeCombName : 'Eigene'}</option>
|
||||
{combinations.length > 0 && <option disabled>──────────</option>}
|
||||
{combinations.map(p => (
|
||||
<option key={p.name} value={p.name}>{p.name}</option>
|
||||
))}
|
||||
{activeCombName && combinations.some(p => p.name === activeCombName) && (
|
||||
<>
|
||||
<option disabled>──────────</option>
|
||||
<option value="__delete__">🗑 Aktuelle löschen</option>
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
<button
|
||||
className="btn-icon-sm"
|
||||
onClick={() => onSaveCurrentCombination && onSaveCurrentCombination()}
|
||||
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
|
||||
>
|
||||
<Icon name="add" size={14} />
|
||||
</button>
|
||||
<button
|
||||
className="btn-icon-sm"
|
||||
onClick={() => onEditCombinations && onEditCombinations()}
|
||||
title="Alle Kombinationen bearbeiten (Dialog)"
|
||||
>
|
||||
<Icon name="edit" size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 4,
|
||||
padding: '6px 14px',
|
||||
background: 'var(--bg-section)',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<span className="label-xs">Sichtbarkeit</span>
|
||||
<select value={mode} onChange={ev => onModeChange(ev.target.value)} style={{ width: '100%' }}>
|
||||
{MODES.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 5,
|
||||
padding: '2px 14px',
|
||||
background: 'var(--bg-section)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
{/* Master-Eye: alle Ebenen sichtbar/unsichtbar */}
|
||||
<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 (onUserVisibilityChange) onUserVisibilityChange()
|
||||
}}
|
||||
title={ebenen.every(e => e.visible !== false)
|
||||
? 'Alle Ebenen ausblenden'
|
||||
: 'Alle Ebenen einblenden'}
|
||||
style={{ width: 18, height: 18 }}
|
||||
>
|
||||
<Icon
|
||||
name={ebenen.every(e => e.visible !== false) ? 'visibility' : 'visibility_off'}
|
||||
size={12}
|
||||
/>
|
||||
</button>
|
||||
<SortHeader label="Cd" sortKey="code" sortBy={sortBy} sortDir={sortDir} onSort={toggleSort} style={{ width: 24 }} />
|
||||
<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 })))
|
||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
||||
}}
|
||||
title={ebenen.every(e => e.locked === true)
|
||||
? 'Alle Ebenen entsperren'
|
||||
: 'Alle Ebenen sperren'}
|
||||
style={{ width: 18, height: 18 }}
|
||||
>
|
||||
<Icon
|
||||
name={ebenen.every(e => e.locked === true) ? 'lock' : 'lock_open'}
|
||||
size={11}
|
||||
/>
|
||||
</button>
|
||||
<div style={{ width: 18 }} />
|
||||
</div>
|
||||
|
||||
{sortedEbenen.map(e => (
|
||||
<EbeneRow
|
||||
key={e.code}
|
||||
e={e}
|
||||
active={e.code === activeCode}
|
||||
mode={mode}
|
||||
onClick={() => onActiveChange(e.code)}
|
||||
onContextMenu={(ev) => openContextMenu(ev, e.code)}
|
||||
onToggleVisible={() => handleToggleVisible(e.code)}
|
||||
onToggleLock={() => handleToggleLock(e.code)}
|
||||
onColorChange={(c) => handleColorChange(e.code, c)}
|
||||
onLwChange={(lw) => handleLwChange(e.code, lw)}
|
||||
onNameChange={(n) => handleNameChange(e.code, n)}
|
||||
onCodeChange={(c) => handleCodeChange(e.code, c)}
|
||||
onDelete={() => handleDelete(e.code)}
|
||||
autoEditCode={autoEdit && autoEdit.code === e.code && autoEdit.field === 'code' ? autoEdit.token : null}
|
||||
autoEditName={autoEdit && autoEdit.code === e.code && autoEdit.field === 'name' ? autoEdit.token : null}
|
||||
/>
|
||||
))}
|
||||
|
||||
{ctxMenu && (
|
||||
<ContextMenu
|
||||
x={ctxMenu.x} y={ctxMenu.y}
|
||||
items={ctxItems(ctxMenu.code)}
|
||||
onClose={() => setCtxMenu(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteTarget && (
|
||||
<ConfirmDeleteEbene
|
||||
ebene={ebenen.find(e => e.code === deleteTarget)}
|
||||
otherEbenen={ebenen.filter(e => e.code !== deleteTarget)}
|
||||
onConfirm={confirmDelete}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{settingsCode && (() => {
|
||||
const target = ebenen.find(e => e.code === settingsCode)
|
||||
if (!target) { setSettingsCode(null); return null }
|
||||
return (
|
||||
<EbenenSettingsDialog
|
||||
ebene={target}
|
||||
hatchPatterns={hatchPatterns}
|
||||
onSave={(updated) => {
|
||||
// Code-Wechsel handhaben (eindeutiger key)
|
||||
onChange(ebenen.map(e => e.code === settingsCode ? updated : e))
|
||||
setSettingsCode(null)
|
||||
}}
|
||||
onClose={() => setSettingsCode(null)}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user