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>
This commit is contained in:
2026-05-19 03:58:28 +02:00
parent e3918cb155
commit 95031ee2c0
29 changed files with 1708 additions and 713 deletions
+74 -198
View File
@@ -3,14 +3,13 @@ import Icon from './components/Icon'
import ContextMenu from './components/ContextMenu'
import {
onMessage, notifyReady,
listLayouts, newLayout, deleteLayout, renameLayout, activateLayout,
listLayouts, deleteLayout, renameLayout, activateLayout,
addDetail, deleteDetail, bindAusschnitt, syncDetail, syncLayout,
setPageSize, exportPdf, exportPdfAll, exportPdfMany,
exportPdf, exportPdfAll, exportPdfMany,
addLayoutFolder, removeLayoutFolder, setLayoutFolder,
openLayoutDialog,
} from './lib/rhinoBridge'
const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter']
const PAPER_FORMATS_MM = {
A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420],
A4: [210, 297], Letter: [216, 279],
@@ -112,7 +111,6 @@ function EditableName({ value, onCommit, style, title, forceEdit, onEditDone })
export default function LayoutsApp() {
const [state, setState] = useState({ layouts: [], snapshots: [], details: {}, folders: [] })
const [selectedId, setSelectedId] = useState(null)
const [dialog, setDialog] = useState(null) // { mode: 'new' | 'edit', layout? }
const [checked, setChecked] = useState(new Set()) // Multi-Select IDs
const [collapsedFolders, setCollapsedFolders] = useState(new Set())
const [draggingId, setDraggingId] = useState(null)
@@ -187,8 +185,10 @@ export default function LayoutsApp() {
{ divider: true },
{ label: 'Als PDF exportieren', icon: 'picture_as_pdf',
onClick: () => exportPdf(l.id, 300) },
{ label: 'Papierformat aendern', icon: 'aspect_ratio',
onClick: () => setDialog({ mode: 'edit', layout: l }) },
{ label: 'Papierformat ändern', icon: 'aspect_ratio',
onClick: () => openLayoutDialog('edit', {
id: l.id, name: l.name, width: l.widthMm, height: l.heightMm,
}) },
{ divider: true },
...(folders.length > 0 ? [
...folders.map(f => ({
@@ -276,57 +276,6 @@ export default function LayoutsApp() {
background: 'var(--bg-base)', color: 'var(--text-primary)',
fontFamily: 'var(--font)', fontSize: 11,
}}>
{/* Header */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 10px',
borderBottom: '1px solid var(--border)',
}}>
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em' }}>LAYOUTS</span>
<button
onClick={handleExportSelection}
className="btn-icon-tonal"
disabled={checked.size === 0}
title={checked.size > 0
? `Auswahl (${checked.size}) als ein PDF exportieren`
: 'Erst Layouts ankreuzen'}
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
>
<Icon name="picture_as_pdf" size={14} />
{checked.size > 0 && <span style={{ fontSize: 10 }}>({checked.size})</span>}
</button>
<button
onClick={() => exportPdfAll(300)}
className="btn-icon-tonal"
disabled={layouts.length === 0}
title="Alle Layouts als ein PDF exportieren"
>
<Icon name="picture_as_pdf" size={14} />
<span style={{ fontSize: 9, marginLeft: 2 }}>·</span>
</button>
<button
onClick={handleNewFolder}
className="btn-icon-tonal"
title="Neuer Ordner"
>
<Icon name="create_new_folder" size={14} />
</button>
<button
onClick={() => setDialog({ mode: 'new' })}
className="btn-add"
title="Neues Layout erstellen"
>
<Icon name="add" size={16} />
</button>
<button
onClick={() => listLayouts()}
className="btn-icon-tonal"
title="Aktualisieren"
>
<Icon name="refresh" size={14} />
</button>
</div>
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 8 }}>
{/* Layout-Liste */}
{layouts.length === 0 && folders.length === 0 ? (
@@ -340,7 +289,7 @@ export default function LayoutsApp() {
<Icon name="dashboard" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
<div style={{ marginTop: 8 }}>Noch keine Layouts.</div>
<div style={{ marginTop: 4, fontSize: 10 }}>
Oben <Icon name="add" size={11} /> klicken um ein neues Layout anzulegen.
Unten <Icon name="add" size={11} /> klicken um ein neues Layout anzulegen.
</div>
</div>
) : (
@@ -610,6 +559,72 @@ export default function LayoutsApp() {
)}
</div>
{/* Sticky Footer: Anzahl + Aktionen */}
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 10px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-panel)',
flexShrink: 0,
}}>
<span className="chip" style={{
fontSize: 9, minWidth: 22, justifyContent: 'center',
}}>{layouts.length}</span>
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
Layouts
</span>
{/* PDF-Aktionen: feste Breite damit das Auswahl-Counter den Footer
nicht horizontal verschiebt. */}
<button
onClick={handleExportSelection}
className="btn-icon-tonal"
disabled={checked.size === 0}
title={checked.size > 0
? `Auswahl (${checked.size}) als ein PDF exportieren`
: 'Erst Layouts ankreuzen'}
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
>
<Icon name="picture_as_pdf" size={14} />
{checked.size > 0 && (
<span style={{ fontSize: 9, fontFamily: 'DM Mono, monospace' }}>
{checked.size}
</span>
)}
</button>
<button
onClick={() => exportPdfAll(300)}
className="btn-icon-tonal"
disabled={layouts.length === 0}
title="Alle Layouts als ein PDF exportieren"
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
>
<Icon name="picture_as_pdf" size={14} />
<span style={{ fontSize: 9 }}>·</span>
</button>
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
<button
onClick={handleNewFolder}
className="btn-icon-tonal"
title="Neuer Ordner"
>
<Icon name="create_new_folder" size={14} />
</button>
<button
onClick={() => listLayouts()}
className="btn-icon-tonal"
title="Aktualisieren"
>
<Icon name="refresh" size={14} />
</button>
<button
onClick={() => openLayoutDialog('new', null)}
className="btn-add"
title="Neues Layout erstellen"
>
<Icon name="add" size={16} />
</button>
</div>
{/* Kontextmenue */}
{ctxMenu && (
<ContextMenu
@@ -627,145 +642,6 @@ export default function LayoutsApp() {
/>
)}
{/* Layout-Dialog: New oder Edit (Papierformat aendern) */}
{dialog && (
<LayoutDialog
mode={dialog.mode}
layout={dialog.layout}
onCancel={() => setDialog(null)}
onSubmit={(p) => {
if (dialog.mode === 'new') {
newLayout(p.name, p.format, p.landscape, p.customWidth, p.customHeight)
} else {
setPageSize(dialog.layout.id, p.format, p.landscape, p.customWidth, p.customHeight)
}
setDialog(null)
}}
/>
)}
</div>
)
}
function LayoutDialog({ mode, layout, onCancel, onSubmit }) {
const editing = mode === 'edit'
const [name, setName] = useState('')
const [format, setFormat] = useState('A3')
const [landscape, setLandscape] = useState(true)
const [cw, setCw] = useState('420') // mm
const [ch, setCh] = useState('297') // mm
// Wenn editieren: aktuelle Layout-Groesse pre-fillen (custom-Mode default)
useEffect(() => {
if (editing && layout) {
// BBox in Doc-Einheiten — wir kennen die Einheit nicht direkt im
// Frontend. Fuer den Edit-Modus zeigen wir die Groesse als Zahlen an
// und schicken sie als "custom" mit der mm-Annahme. Wenn das Doc nicht
// auf mm steht, ergibt sich eine kleine Konvertier-Unschaerfe — das
// Backend rechnet mm-Werte konsistent in Doc-Units um.
setFormat('custom')
setCw(String(Math.round(layout.width)))
setCh(String(Math.round(layout.height)))
}
}, [editing, layout])
return (
<div style={{
position: 'fixed', inset: 0,
background: 'rgba(0,0,0,0.55)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 100,
}} onClick={(e) => { if (e.target === e.currentTarget) onCancel() }}>
<div style={{
width: 340, background: 'var(--bg-base)',
border: '1px solid var(--border)', borderRadius: 'var(--r-lg)',
display: 'flex', flexDirection: 'column', overflow: 'hidden',
}}>
<div style={{ padding: '10px 12px', borderBottom: '1px solid var(--border)',
fontWeight: 600 }}>
{editing ? `Papierformat: ${layout?.name}` : 'Neues Layout'}
</div>
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
{!editing && (
<div>
<div style={labelXs}>Name</div>
<input type="text" value={name} onChange={(e) => setName(e.target.value)}
placeholder="z.B. Grundriss EG"
autoFocus
style={{ width: '100%', marginTop: 4 }} />
</div>
)}
<div>
<div style={labelXs}>Papierformat</div>
<div style={{ display: 'flex', gap: 4, marginTop: 4, flexWrap: 'wrap' }}>
{PAPER_SIZES.map(f => (
<button key={f}
onClick={() => setFormat(f)}
className={format === f ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 10px', fontSize: 11 }}>
{f}
</button>
))}
<button
onClick={() => setFormat('custom')}
className={format === 'custom' ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 10px', fontSize: 11 }}>
Eigene
</button>
</div>
</div>
{format === 'custom' ? (
<div>
<div style={labelXs}>Eigene Groesse (mm)</div>
<div style={{ display: 'flex', gap: 6, marginTop: 4, alignItems: 'center' }}>
<input type="text" value={cw} onChange={(e) => setCw(e.target.value)}
placeholder="Breite"
style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} />
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>×</span>
<input type="text" value={ch} onChange={(e) => setCh(e.target.value)}
placeholder="Höhe"
style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} />
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>mm</span>
</div>
</div>
) : (
<div>
<div style={labelXs}>Ausrichtung</div>
<div style={{ display: 'flex', gap: 4, marginTop: 4 }}>
<button
onClick={() => setLandscape(true)}
className={landscape ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 10px', fontSize: 11, display: 'flex', gap: 4, alignItems: 'center' }}>
<Icon name="crop_landscape" size={12} /> Quer
</button>
<button
onClick={() => setLandscape(false)}
className={!landscape ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 10px', fontSize: 11, display: 'flex', gap: 4, alignItems: 'center' }}>
<Icon name="crop_portrait" size={12} /> Hoch
</button>
</div>
</div>
)}
</div>
<div style={{ padding: 10, borderTop: '1px solid var(--border)',
display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
<button onClick={onCancel}>Abbrechen</button>
<button className="btn-contained"
onClick={() => {
const payload = { name: name.trim(), format, landscape }
if (format === 'custom') {
const w = parseFloat(cw), h = parseFloat(ch)
if (!(w > 0) || !(h > 0)) { alert('Bitte gueltige Groesse eingeben.'); return }
payload.customWidth = w
payload.customHeight = h
}
onSubmit(payload)
}}>
{editing ? 'Anwenden' : 'Erstellen'}
</button>
</div>
</div>
</div>
)
}