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:
+74
-198
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user