Files
DOSSIER/src/LayoutDialogApp.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

208 lines
7.4 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 { useEffect, useState } from 'react'
import Icon from './components/Icon'
import { onMessage, notifyReady } from './lib/rhinoBridge'
function send(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[LayoutDialog] →', type, payload); return }
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
}
const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter']
export default function LayoutDialogApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
const [mode, setMode] = useState(initial.mode || 'new')
const [layout, setLayout] = useState(initial.layout || null)
const [name, setName] = useState('')
const [format, setFormat] = useState('A3')
const [landscape, setLandscape] = useState(true)
const [cw, setCw] = useState('420')
const [ch, setCh] = useState('297')
useEffect(() => {
onMessage('LAYOUT_DIALOG_STATE', (p) => {
if (p.mode) setMode(p.mode)
if (p.layout) {
setLayout(p.layout)
if (p.mode === 'edit') {
setFormat('custom')
setCw(String(Math.round(p.layout.width || 420)))
setCh(String(Math.round(p.layout.height || 297)))
}
}
})
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
const editing = mode === 'edit'
const submit = () => {
const payload = { name: name.trim(), format, landscape }
if (format === 'custom') {
const w = parseFloat(cw), h = parseFloat(ch)
if (!(w > 0) || !(h > 0)) { alert('Bitte gültige Größe eingeben.'); return }
payload.customWidth = w
payload.customHeight = h
}
send('SAVE', payload)
}
return (
<div style={{
position: 'absolute', inset: 0,
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
fontFamily: 'var(--font)', color: 'var(--text-primary)',
overflow: 'hidden',
}}>
{/* Body */}
<div style={{ flex: 1, overflowY: 'auto', padding: '12px 16px',
display: 'flex', flexDirection: 'column', gap: 14 }}>
{!editing && (
<Field label="Name">
<input
type="text" value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') submit() }}
placeholder="z.B. Grundriss EG"
autoFocus
style={{ width: '100%', fontSize: 12, padding: '6px 8px' }}
/>
</Field>
)}
<Field label="Papierformat">
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{PAPER_SIZES.map(f => (
<button key={f}
onClick={() => setFormat(f)}
className={format === f ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '5px 12px', fontSize: 11 }}>
{f}
</button>
))}
<button
onClick={() => setFormat('custom')}
className={format === 'custom' ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '5px 12px', fontSize: 11 }}>
Eigene
</button>
</div>
</Field>
{format === 'custom' ? (
<Field label="Größe (mm)">
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input
type="text" value={cw}
onChange={(e) => setCw(e.target.value)}
placeholder="Breite"
style={{ flex: 1, fontFamily: 'DM Mono, monospace',
fontSize: 12, textAlign: 'right', padding: '6px 8px' }}
/>
<span style={{ color: 'var(--text-muted)', fontSize: 11 }}>×</span>
<input
type="text" value={ch}
onChange={(e) => setCh(e.target.value)}
placeholder="Höhe"
style={{ flex: 1, fontFamily: 'DM Mono, monospace',
fontSize: 12, textAlign: 'right', padding: '6px 8px' }}
/>
<span style={{ color: 'var(--text-muted)', fontSize: 10, width: 22 }}>mm</span>
</div>
</Field>
) : (
<Field label="Ausrichtung">
<div style={{ display: 'flex', gap: 6 }}>
<button
onClick={() => setLandscape(true)}
className={landscape ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '8px 12px', fontSize: 11,
display: 'flex', gap: 6, alignItems: 'center',
justifyContent: 'center' }}>
<Icon name="crop_landscape" size={16} /> Quer
</button>
<button
onClick={() => setLandscape(false)}
className={!landscape ? 'btn-contained' : 'btn-outlined'}
style={{ flex: 1, padding: '8px 12px', fontSize: 11,
display: 'flex', gap: 6, alignItems: 'center',
justifyContent: 'center' }}>
<Icon name="crop_portrait" size={16} /> Hoch
</button>
</div>
</Field>
)}
{/* Preview */}
<FormatPreview format={format} landscape={landscape}
customW={parseFloat(cw)} customH={parseFloat(ch)} />
</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 }} />
<button className="btn-text" onClick={() => send('CANCEL', {})}>Abbrechen</button>
<button className="btn-contained" onClick={submit}
disabled={!editing && !name.trim()}
title={!editing && !name.trim() ? 'Erst einen Namen eingeben' : ''}>
{editing ? 'Anwenden' : 'Erstellen'}
</button>
</div>
</div>
)
}
function Field({ label, children }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<span className="label-xs">{label}</span>
{children}
</div>
)
}
const PAPER_DIMS = {
A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420],
A4: [210, 297], Letter: [216, 279],
}
function FormatPreview({ format, landscape, customW, customH }) {
let w, h
if (format === 'custom') { w = customW; h = customH }
else {
const dims = PAPER_DIMS[format]
if (!dims) return null
w = dims[0]; h = dims[1]
if (landscape) { w = dims[1]; h = dims[0] }
}
if (!(w > 0) || !(h > 0)) return null
const MAX = 120
const scale = Math.min(MAX / w, MAX / h)
const pw = w * scale, ph = h * scale
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, marginTop: 6 }}>
<div style={{
width: pw, height: ph,
background: 'var(--bg-input)',
border: '1.5px solid var(--accent)',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
transition: 'width 0.2s, height 0.2s',
}} />
<span style={{ fontSize: 10, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace' }}>
{Math.round(w)} × {Math.round(h)} mm
</span>
</div>
)
}