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
+53 -79
View File
@@ -50,24 +50,22 @@ function NumInput({ value, onCommit, disabled, suffix, width }) {
)
}
// 9-Punkt-Referenzpunkt-Selektor im Illustrator-Stil: sichtbarer BBox-Rahmen,
// die Punkte sitzen AUF Ecken / Kantenmitten / Zentrum.
// 9-Punkt-Referenzpunkt-Selektor: sichtbarer BBox-Rahmen, Kreise auf den
// Eckpunkten / Kantenmitten / Zentrum.
function RefPointGrid({ ref, onChange }) {
const SIZE = 26 // Aussenkanten-Quadrat (px)
const DOT = 5 // Punkt-Durchmesser (px)
// Position pro Code: 0% (min), 50% (mid), 100% (max)
const SIZE = 28 // Aussenkanten-Quadrat (px)
const DOT = 6 // Kreis-Durchmesser (px)
const pct = (c) => c === 'min' ? '0%' : c === 'max' ? '100%' : '50%'
return (
<div style={{
position: 'relative',
width: SIZE, height: SIZE,
border: '1px solid var(--text-muted)',
border: '1px solid var(--border)',
background: 'transparent',
flexShrink: 0,
}}>
{REF_CODES.map(yc => REF_CODES.map(xc => {
const active = ref.x === xc && ref.y === yc
// yc 'max' = top in user mental model (Vectorworks/Illustrator)
const topPct = yc === 'max' ? '0%' : yc === 'min' ? '100%' : '50%'
return (
<button
@@ -79,14 +77,20 @@ function RefPointGrid({ ref, onChange }) {
left: pct(xc), top: topPct,
transform: 'translate(-50%, -50%)',
width: DOT, height: DOT, padding: 0,
borderRadius: 0, // eckig wie Illustrator
borderRadius: '50%',
background: active ? 'var(--accent)' : 'var(--text-muted)',
border: 'none',
cursor: 'pointer',
transition: 'background 0.1s',
transition: 'background 0.12s, transform 0.12s',
}}
onMouseEnter={(e) => {
if (!active) e.currentTarget.style.background = 'var(--text-primary)'
e.currentTarget.style.transform = 'translate(-50%, -50%) scale(1.25)'
}}
onMouseLeave={(e) => {
if (!active) e.currentTarget.style.background = 'var(--text-muted)'
e.currentTarget.style.transform = 'translate(-50%, -50%) scale(1)'
}}
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--text-primary)' }}
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'var(--text-muted)' }}
/>
)
}))}
@@ -189,15 +193,13 @@ export default function DimensionenApp() {
background: 'var(--bg-base)', color: 'var(--text-primary)',
fontFamily: 'var(--font)', fontSize: 11,
}}>
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 6 }}>
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden' }}>
{/* Header: Selektions-Info + World/CPlane */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6,
padding: '5px 8px',
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<Icon name="select_all" size={14} style={{ color: 'var(--text-muted)' }} />
<span style={{ flex: 1, fontWeight: 500 }}>{selLabel()}</span>
@@ -221,11 +223,8 @@ export default function DimensionenApp() {
<div style={{
padding: '32px 16px', textAlign: 'center',
color: 'var(--text-muted)', fontSize: 11,
border: '1px dashed var(--border)',
borderRadius: 'var(--r-lg)',
background: 'var(--bg-section)',
}}>
<Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
<Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.4 }} />
<div style={{ marginTop: 8 }}>Keine Selektion.</div>
<div style={{ marginTop: 4, fontSize: 10 }}>
In Rhino ein oder mehrere Objekte auswählen.
@@ -233,38 +232,30 @@ export default function DimensionenApp() {
</div>
) : (
<>
{/* Referenzpunkt — kompakte einzeilige Card */}
{/* Referenzpunkt */}
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '6px 8px', marginBottom: 6,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
display: 'flex', alignItems: 'center', gap: 10,
padding: '10px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
Ref
</span>
<span className="label-xs" style={{ width: 30 }}>Ref</span>
<RefPointGrid ref={ref} onChange={onRefChange} />
<div style={{ flex: 1 }} />
<RefZSelector z={ref.z} onChange={(z) => onRefChange({ ...ref, z })} />
</div>
{/* Position + Abmessungen nebeneinander */}
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
<div style={{
flex: 1, display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 8px',
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
minWidth: 0,
}}>
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase',
display: 'flex', justifyContent: 'space-between' }}>
<span>Position</span>
<span style={{ fontWeight: 400, textTransform: 'none',
fontFamily: 'DM Mono, monospace', fontSize: 9 }}>
{/* Position + BBox nebeneinander */}
<div style={{
display: 'flex', gap: 16,
padding: '10px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between',
alignItems: 'baseline', marginBottom: 2 }}>
<span className="label-xs">Position</span>
<span style={{ fontFamily: 'DM Mono, monospace', fontSize: 9,
color: 'var(--text-muted)' }}>
{state.planeName}
</span>
</div>
@@ -272,18 +263,9 @@ export default function DimensionenApp() {
<Field label="Y"><NumInput value={pos.y} onCommit={(v) => setDimPosition('y', v)} /></Field>
<Field label="Z"><NumInput value={pos.z} onCommit={(v) => setDimPosition('z', v)} /></Field>
</div>
<div style={{
flex: 1, display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 8px',
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
minWidth: 0,
}}>
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
BBox
</div>
<div style={{ width: 1, background: 'var(--border-light)' }} />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
<span className="label-xs" style={{ marginBottom: 2 }}>BBox</span>
<Field label="B"><NumInput value={dims?.width} onCommit={(v) => setDimDimension('width', v)} /></Field>
<Field label="T"><NumInput value={dims?.depth} onCommit={(v) => setDimDimension('depth', v)} /></Field>
<Field label="H"><NumInput value={dims?.height} onCommit={(v) => setDimDimension('height', v)} /></Field>
@@ -294,22 +276,19 @@ export default function DimensionenApp() {
{shape && (
<div style={{
display: 'flex', flexDirection: 'column', gap: 4,
padding: '6px 8px', marginBottom: 6,
background: 'var(--bg-section)',
border: '1px solid var(--accent)',
borderRadius: 'var(--r-lg)',
padding: '10px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--accent)',
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
<span className="label-xs" style={{ color: 'var(--accent)' }}>
{shape.type === 'circle' && 'Kreis'}
{shape.type === 'rectangle' && 'Rechteck'}
{shape.type === 'line' && 'Linie'}
</div>
</span>
{shape.type === 'circle' && (
<Field label="R"><NumInput value={shape.radius} onCommit={(v) => setCircleRadius(v)} /></Field>
)}
{shape.type === 'rectangle' && (
<div style={{ display: 'flex', gap: 6 }}>
<div style={{ display: 'flex', gap: 8 }}>
<Field label="W" style={{ flex: 1 }}>
<NumInput value={shape.width}
onCommit={(v) => setRectangleDims(v, shape.height)} />
@@ -321,7 +300,7 @@ export default function DimensionenApp() {
</div>
)}
{shape.type === 'line' && (
<div style={{ display: 'flex', gap: 6 }}>
<div style={{ display: 'flex', gap: 8 }}>
<Field label="L" style={{ flex: 1 }}>
<NumInput value={shape.length} onCommit={(v) => setLineLength(v)} />
</Field>
@@ -333,19 +312,13 @@ export default function DimensionenApp() {
</div>
)}
{/* Rotation — kompakt einzeilig */}
{/* Rotation */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '6px 8px', marginBottom: 6,
background: 'var(--bg-section)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
padding: '10px 12px',
}}>
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
Drehen
</span>
<div style={{ flex: 1 }}>
<span className="label-xs" style={{ width: 50 }}>Drehen</span>
<div style={{ width: 56 }}>
<NumInput value={rotationDelta} onCommit={setRotationDelta} suffix="°" />
</div>
<button
@@ -357,12 +330,13 @@ export default function DimensionenApp() {
>
<Icon name="rotate_right" size={13} />
</button>
<div style={{ flex: 1 }} />
{[-90, -45, 45, 90].map(a => (
<button
key={a}
className="btn-outlined"
onClick={() => setDimRotationZ(a)}
style={{ padding: '3px 5px', fontSize: 9, minWidth: 28 }}
style={{ padding: '3px 6px', fontSize: 9, minWidth: 28 }}
title={`${a}°`}
>
{a > 0 ? '+' : ''}{a}°