Files
DOSSIER/src/components/AusschnittLayerDialog.jsx
T
karim 95031ee2c0 Panels poliert: Ebenenkombi in Oberleiste, Satelliten-Dialoge, Caps weg, Perf
- Ebenenkombination raus aus Ebenen-Panel, in Oberleiste-Topbar +
  Editor-Satellite (AusschnittLayerDialog embedded). doc.Strings
  haelt active_comb_name, auto-clear bei manueller Eye/Lock-Aenderung.
- EbenenSettingsDialog jetzt Satellite mit Ebene-Picker-Dropdown
  (auto-save on switch via SAVE_KEEP).
- Per-Ausschnitt Einstellungen-Satellite (Massstab, Display, Overrides,
  Ebenenkombi). Alte 'Sichtbarkeit bearbeiten'-Option entfernt.
- Layouts/Ausschnitte: Top-Header weg, Sticky-Footer mit Anzahl +
  Aktionen. LayoutDialog ist jetzt Satellite mit Format-Live-Preview.
- Panel-Captions + Default-Ebenen-Namen auf Mixed-Case (Ausschnitte,
  Ebenen, Waende ...). Nur DOSSIER bleibt caps.
- DimensionenApp: Card-Optik raus, REF-Wuerfel mit Kreisen statt
  Quadraten + Hover-Scale.
- GeschossManager angeglichen an EbenenManager: Rechtsklick-Menue,
  Lock-Button, Delete-X, Duplizieren. layer_builder honoriert z.locked.
- Active Sublayer folgt jetzt dem Geschoss-Wechsel (gleicher Code
  unter neuem Parent).

Performance Geschoss-Wechsel:
- elemente._send_state() ersetzt durch _notify_active_geschoss()
  (Partial-Push statt 200+ Elements re-enumerieren).
- _apply_visibility dedupe via sticky last-applied-signature
  (STATE_SYNC-Echo loopt nicht mehr durch alle Layer).
- _update_clipping nur wenn alt oder neu hasClipping=True.
- Redundante doc.Views.Redraw() im CPlane-Pfad entfernt — die folgende
  apply_visibility-Roundtrip redrawt 30ms spaeter ohnehin.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 03:58:28 +02:00

311 lines
12 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useMemo, useEffect } from 'react'
import Icon from './Icon'
// Erzeugt ein vollstaendiges Draft-Array fuer einen Preset.
// Layer die im Preset nicht enthalten sind werden auf Default (visible=true,
// locked=false) gesetzt — sonst gibt es Schmutz wenn man zwischen Presets
// hin- und herwechselt.
function draftFromPreset(allLayers, preset) {
if (!preset || !preset.layers) {
return allLayers.map(l => ({ ...l }))
}
const map = {}
;(preset.layers || []).forEach(l => { map[l.id] = l })
return allLayers.map(l => {
const ps = map[l.id]
if (ps) return { ...l, visible: !!ps.visible, locked: !!ps.locked }
return { ...l, visible: true, locked: false }
})
}
export default function AusschnittLayerDialog({
snapName, layers, presets,
onSave, onClose,
onSavePreset, onDeletePreset,
embedded = false,
}) {
// Welche Kombination wird gerade angezeigt? null = aktueller Doc-State
const [selectedPreset, setSelectedPreset] = useState(null)
const [draft, setDraft] = useState(() => layers.map(l => ({ ...l })))
const [dirty, setDirty] = useState(false)
const [filter, setFilter] = useState('')
const [newName, setNewName] = useState('')
// Wenn die Layer-Liste (von Backend) sich aendert wegen Doc-Update,
// resetten wir den Draft — aber nur wenn nicht dirty.
useEffect(() => {
if (dirty) return
if (selectedPreset === null) {
setDraft(layers.map(l => ({ ...l })))
} else {
const p = presets.find(p => p.name === selectedPreset)
setDraft(draftFromPreset(layers, p))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [layers])
const filtered = useMemo(() => {
const f = filter.trim().toLowerCase()
if (!f) return draft
return draft.filter(l => (l.fullPath || l.name || '').toLowerCase().includes(f))
}, [draft, filter])
const pickPreset = (name) => {
if (dirty && !window.confirm('Ungespeicherte Änderungen verwerfen?')) return
setSelectedPreset(name || null)
setDirty(false)
if (!name) {
setDraft(layers.map(l => ({ ...l })))
} else {
const p = presets.find(p => p.name === name)
setDraft(draftFromPreset(layers, p))
}
}
const toggle = (id, field) => {
setDraft(d => d.map(l => l.id === id ? { ...l, [field]: !l[field] } : l))
setDirty(true)
}
const setAll = (field, value) => {
setDraft(d => d.map(l => filtered.find(f => f.id === l.id) ? { ...l, [field]: value } : l))
setDirty(true)
}
const savePresetChanges = () => {
if (!selectedPreset) return
if (!window.confirm(`Änderungen an Kombination "${selectedPreset}" speichern?`)) return
onSavePreset(selectedPreset, draft.map(l => ({ id: l.id, visible: l.visible, locked: l.locked })))
setDirty(false)
}
const saveAsNew = () => {
const name = newName.trim()
if (!name) return
if (presets.some(p => p.name === name) &&
!window.confirm(`Kombination "${name}" überschreiben?`)) return
onSavePreset(name, draft.map(l => ({ id: l.id, visible: l.visible, locked: l.locked })))
setSelectedPreset(name)
setDirty(false)
setNewName('')
}
const deleteSelected = () => {
if (!selectedPreset) return
if (!window.confirm(`Kombination "${selectedPreset}" löschen?`)) return
onDeletePreset(selectedPreset)
setSelectedPreset(null)
setDirty(false)
setDraft(layers.map(l => ({ ...l })))
}
const applyToDoc = () => {
if (dirty && selectedPreset &&
!window.confirm('Änderungen an dieser Kombination sind nicht gespeichert. Trotzdem auf das Dokument anwenden?')) return
onSave(draft)
}
const wrapperStyle = embedded
? { position: 'absolute', inset: 0, display: 'flex' }
: { position: 'absolute', inset: 0, zIndex: 150,
background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 30 }
const cardStyle = embedded
? { flex: 1, display: 'flex', flexDirection: 'column',
background: 'var(--bg-dialog)', overflow: 'hidden' }
: { background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
boxShadow: 'var(--shadow-3)',
width: 'calc(100% - 24px)', maxWidth: 480,
maxHeight: 'calc(100vh - 60px)',
display: 'flex', flexDirection: 'column',
overflow: 'hidden' }
return (
<div style={wrapperStyle}>
<div style={cardStyle}>
{/* Header — im embedded-Modus weggelassen (Satellite-Fenster hat schon
seine native Title-Bar mit Close-Button) */}
{!embedded && (
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '12px 16px',
borderBottom: '1px solid var(--border)',
}}>
<Icon name="layers" size={16} style={{ color: 'var(--text-secondary)' }} />
<span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}>
{snapName}
</span>
{dirty && (
<span style={{ fontSize: 10, color: 'var(--warn)',
padding: '2px 6px', borderRadius: 'var(--r)',
background: 'var(--warn-dim)' }}
title="Ungespeicherte Änderungen"></span>
)}
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
</div>
)}
{/* Preset-Auswahl */}
<div style={{
padding: '10px 14px',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
display: 'flex', flexDirection: 'column', gap: 8,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span className="label-xs" style={{ flexShrink: 0 }}>Kombination</span>
<select
value={selectedPreset || ''}
onChange={(ev) => pickPreset(ev.target.value || null)}
style={{ flex: 1, fontSize: 11 }}
>
<option value=""> Aktueller Zustand </option>
{presets.length > 0 && <option disabled></option>}
{presets.map(p => (
<option key={p.name} value={p.name}>
{p.name} ({p.layers?.length || 0} Ebenen)
</option>
))}
</select>
{selectedPreset && (
<button
className="btn-icon-sm"
onClick={deleteSelected}
title="Diese Kombination löschen"
style={{ color: 'var(--danger)' }}
>
<Icon name="delete" size={13} />
</button>
)}
</div>
{selectedPreset && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<button
className="btn-contained"
onClick={savePresetChanges}
disabled={!dirty}
style={{ fontSize: 10, padding: '3px 10px',
opacity: dirty ? 1 : 0.5 }}
title={dirty ? `Änderungen in "${selectedPreset}" speichern` : 'Keine Änderungen'}
>
<Icon name="save" size={12} />
<span>Speichern</span>
</button>
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
Änderungen werden NICHT automatisch gespeichert.
</span>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<input
value={newName}
onChange={(ev) => setNewName(ev.target.value)}
onKeyDown={(ev) => { if (ev.key === 'Enter') saveAsNew() }}
placeholder="Aktuelle Auswahl als neue Kombination speichern…"
style={{ flex: 1, fontSize: 10 }}
/>
<button
className="btn-outlined"
onClick={saveAsNew}
disabled={!newName.trim()}
title="Aktuelle Auswahl unter diesem Namen speichern"
style={{ fontSize: 10, padding: '3px 8px' }}
>
<Icon name="add" size={12} /> Neu
</button>
</div>
</div>
{/* Such- und Bulk-Zeile */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '6px 14px',
borderBottom: '1px solid var(--border-light)',
}}>
<input
value={filter}
onChange={(ev) => setFilter(ev.target.value)}
placeholder="Filter..."
style={{ flex: 1, fontSize: 10, padding: '3px 6px' }}
/>
<button className="btn-icon-xs" onClick={() => setAll('visible', true)} title="Alle (gefiltert) sichtbar">
<Icon name="visibility" size={12} />
</button>
<button className="btn-icon-xs" onClick={() => setAll('visible', false)} title="Alle (gefiltert) ausblenden">
<Icon name="visibility_off" size={12} />
</button>
<button className="btn-icon-xs" onClick={() => setAll('locked', false)} title="Alle (gefiltert) entsperren">
<Icon name="lock_open" size={12} />
</button>
</div>
{/* Layer-Liste */}
<div style={{ flex: 1, overflowY: 'auto', minHeight: 200,
maxHeight: embedded ? 'none' : '50vh' }}>
{filtered.length === 0 ? (
<div style={{ padding: '30px 14px', textAlign: 'center', color: 'var(--text-muted)', fontSize: 11 }}>
Keine Ebenen gefunden.
</div>
) : (
filtered.map(l => (
<div
key={l.id}
style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '4px 14px',
borderBottom: '1px solid var(--border-light)',
background: 'var(--bg-item)',
opacity: l.visible ? 1 : 0.5,
}}
>
<span style={{
width: 10, height: 10, borderRadius: 2,
background: l.color || '#888',
flexShrink: 0,
border: '1px solid var(--border-light)',
}} />
<span style={{
flex: 1, fontSize: 11,
color: 'var(--text-label)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
fontFamily: 'var(--font-mono)',
}} title={l.fullPath}>{l.fullPath || l.name}</span>
<button
className={`btn-icon-xs ${l.visible ? 'is-on' : ''}`}
onClick={() => toggle(l.id, 'visible')}
title={l.visible ? 'Ausblenden' : 'Einblenden'}
><Icon name={l.visible ? 'visibility' : 'visibility_off'} size={12} /></button>
<button
className={`btn-icon-xs ${l.locked ? 'is-on' : ''}`}
onClick={() => toggle(l.id, 'locked')}
title={l.locked ? 'Entsperren' : 'Sperren'}
><Icon name={l.locked ? 'lock' : 'lock_open'} size={12} /></button>
</div>
))
)}
</div>
{/* Footer */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '10px 14px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-section)',
}}>
<div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}>
{draft.filter(l => l.visible).length} / {draft.length} sichtbar
</div>
<button className="btn-text" onClick={onClose}>Schliessen</button>
<button className="btn-contained" onClick={applyToDoc}
title="Aktuelle Auswahl auf das Dokument anwenden">
<Icon name="check" size={12} />
<span>Auf Doc anwenden</span>
</button>
</div>
</div>
</div>
)
}