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

468 lines
18 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 } from 'react'
import Icon from './Icon'
function Field({ label, hint, children }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: '5px 0' }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', fontWeight: 500, letterSpacing: 0.2 }}>
{label}
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
{hint && (
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4 }}>{hint}</span>
)}
</div>
)
}
function SectionLabel({ children }) {
return (
<div style={{
fontSize: 9, color: 'var(--text-muted)', fontWeight: 600,
letterSpacing: 0.5, textTransform: 'uppercase',
padding: '8px 0 2px',
borderTop: '1px solid var(--border-light)',
marginTop: 6,
}}>{children}</div>
)
}
export default function EbenenSettingsDialog({
ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false,
pickerEbenen = null, pickerSelected = null, onPickEbene = null,
}) {
const [draft, setDraft] = useState({
...ebene,
fill: {
pattern: 'None',
source: 'layer',
color: null,
scale: 1.0,
rotation: 0,
lw: null,
...(ebene.fill || {}),
},
// Section Style: was Rhino bei Clipping-Plane-Schnitten anzeigt.
// Spiegelt das native Section-Style-Dialog (Options → Layer → Section Style).
section: {
hatchPattern: 'None', // None / Solid / <name>
hatchColor: null, // null = ByObject (Layer-Farbe)
hatchRotation: 0,
hatchScale: 1.0,
background: 'viewport', // viewport / object
boundaryShow: true,
boundaryLinetype: 'byLayer',
boundaryWidthScale: 1.0,
boundaryColor: null, // null = ByObject
sectionOpenObjects: true,
...(ebene.section || {}),
},
})
const set = (patch) => setDraft({ ...draft, ...patch })
const setFill = (patch) => setDraft({ ...draft, fill: { ...draft.fill, ...patch } })
const setSection = (patch) => setDraft({ ...draft, section: { ...draft.section, ...patch } })
const fill = draft.fill
const isFilled = fill.pattern !== 'None'
const isPattern = isFilled && fill.pattern !== 'Solid'
const fillFromLayer = fill.source === 'layer'
const previewColor = (fillFromLayer || !fill.color) ? draft.color : fill.color
const sec = draft.section
const secHatched = sec.hatchPattern !== 'None'
const secHatchPat = secHatched && sec.hatchPattern !== 'Solid'
const secHatchColor = sec.hatchColor || draft.color
const secBoundColor = sec.boundaryColor || draft.color
// Pattern-Optionen: None + Solid + Patterns
const patternOptions = [
'None', 'Solid',
...hatchPatterns.filter(p => p !== 'Solid' && p !== 'None'),
]
const wrapperStyle = embedded ? {
width: '100%', height: '100%',
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
position: 'absolute', inset: 0, zIndex: 150,
background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 30,
}
const innerStyle = embedded ? {
width: '100%', height: '100%',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
boxShadow: 'var(--shadow-3)',
width: 'calc(100% - 16px)', maxWidth: 300,
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
maxHeight: 'calc(100vh - 60px)',
}
return (
<div style={wrapperStyle}>
<div style={innerStyle}>
{/* Header — embedded zeigt nur das Ebenen-Picker-Dropdown (kein
Title + kein Close, dafuer hat das Fenster seine native Title-
Bar). Modal-Variante zeigt den klassischen Header. */}
{embedded && pickerEbenen ? (
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 12px',
borderBottom: '1px solid var(--border)',
}}>
<span style={{ fontSize: 9, color: 'var(--text-muted)',
textTransform: 'uppercase', letterSpacing: 0.5 }}>
Ebene
</span>
<select
value={pickerSelected || draft.code}
onChange={(ev) => onPickEbene && onPickEbene(ev.target.value, draft)}
style={{ flex: 1, fontSize: 11, minWidth: 0,
fontFamily: 'var(--font-mono)' }}
title="Zwischen Ebenen wechseln — aktuelle Änderungen werden mit übernommen"
>
{pickerEbenen.map(e => (
<option key={e.code} value={e.code}>
{e.code} {e.name}
</option>
))}
</select>
</div>
) : !embedded && (
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '10px 12px',
borderBottom: '1px solid var(--border)',
}}>
<Icon name="settings" size={14} style={{ color: 'var(--text-secondary)', flexShrink: 0 }} />
<span style={{
flex: 1, fontWeight: 600, fontSize: 11,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{ebene.code} {ebene.name}
</span>
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px', lineHeight: 1 }}>×</button>
</div>
)}
{/* Body */}
<div style={{ padding: '6px 12px 4px', overflowY: 'auto' }}>
<Field label="CODE">
<input
value={draft.code}
onChange={(ev) => set({ code: ev.target.value })}
maxLength={4}
style={{ flex: 1, fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, minWidth: 0 }}
/>
</Field>
<Field label="NAME">
<input
value={draft.name}
onChange={(ev) => set({ name: ev.target.value })}
style={{ flex: 1, fontSize: 11, fontWeight: 600, minWidth: 0 }}
/>
</Field>
<Field label="FARBE (Stift)">
<input
type="color"
value={draft.color}
onChange={(ev) => set({ color: ev.target.value })}
style={{
width: 32, height: 22, padding: 0,
border: '1px solid var(--border)', borderRadius: 'var(--r)',
cursor: 'pointer', background: 'transparent',
}}
/>
<input
value={draft.color}
onChange={(ev) => set({ color: ev.target.value })}
style={{ flex: 1, fontSize: 10, fontFamily: 'var(--font-mono)', minWidth: 0 }}
/>
</Field>
<Field label="STRICHSTÄRKE (mm)">
<input
type="number" step="0.01" min="0.01" max="2.0"
value={draft.lw}
onChange={(ev) => set({ lw: parseFloat(ev.target.value) || draft.lw })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
<SectionLabel>Schraffur Nach Ebene"</SectionLabel>
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4, display: 'block', marginBottom: 4 }}>
Wird auf neue geschlossene Kurven angewandt die auf dieser Ebene gezeichnet werden.
</span>
<Field label="PATTERN">
<select
value={fill.pattern}
onChange={(ev) => setFill({ pattern: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
{patternOptions.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
</Field>
{isFilled && (
<>
<Field label="FARBE-QUELLE">
<select
value={fill.source}
onChange={(ev) => setFill({ source: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value="layer">Nach Stift (Layerfarbe)</option>
<option value="object">Eigene Farbe</option>
</select>
</Field>
<Field label="VORSCHAU / FARBE">
<input
type="color"
value={previewColor}
disabled={fillFromLayer}
onChange={(ev) => setFill({ color: ev.target.value, source: 'object' })}
style={{
width: 32, height: 22, padding: 0,
border: '1px solid var(--border)', borderRadius: 'var(--r)',
cursor: fillFromLayer ? 'default' : 'pointer',
background: 'transparent',
opacity: fillFromLayer ? 0.6 : 1,
}}
/>
<span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-muted)' }}>
{previewColor}
</span>
</Field>
{isPattern && (
<>
<Field label="SKALIERUNG">
<input
type="number" step="0.05" min="0.001"
value={fill.scale}
onChange={(ev) => setFill({ scale: parseFloat(ev.target.value) || 1.0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
<Field label="DREHUNG (°)">
<input
type="number" step="5"
value={fill.rotation}
onChange={(ev) => setFill({ rotation: parseFloat(ev.target.value) || 0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
</>
)}
<Field
label="STIFTSTÄRKE (mm)"
hint="Leer = wie Stift der Ebene. Eigener Wert überschreibt die Strichstärke der Hatch-Linien."
>
<input
type="number" step="0.01" min="0"
value={fill.lw == null ? '' : fill.lw}
placeholder=""
onChange={(ev) => {
const v = ev.target.value
if (v === '' || v === null) setFill({ lw: null })
else {
const f = parseFloat(v)
if (!isNaN(f) && f >= 0) setFill({ lw: f })
}
}}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
<button
className="btn-text"
style={{ fontSize: 10, padding: '2px 6px' }}
onClick={() => setFill({ lw: null })}
title="Auf 'wie Stift der Ebene' zuruecksetzen"
>Default</button>
</Field>
</>
)}
{/* === SECTION STYLE (Clipping-Plane-Schnitt) === */}
<SectionLabel>Section Style (Clipping-Schnitt)</SectionLabel>
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4, display: 'block', marginBottom: 4 }}>
Wird gezeigt wenn ein Objekt auf dieser Ebene von einer Clipping-Plane geschnitten wird.
</span>
<Field label="HATCH PATTERN">
<select
value={sec.hatchPattern}
onChange={(ev) => setSection({ hatchPattern: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
{patternOptions.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
</Field>
{secHatched && (
<>
<Field label="HATCH FARBE" hint="Leer = Stift der Ebene (By Object)">
<input
type="color"
value={secHatchColor}
onChange={(ev) => setSection({ hatchColor: ev.target.value })}
style={{
width: 32, height: 22, padding: 0,
border: '1px solid var(--border)', borderRadius: 'var(--r)',
cursor: 'pointer', background: 'transparent',
}}
/>
<span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-muted)', flex: 1 }}>
{sec.hatchColor || 'By Object'}
</span>
{sec.hatchColor && (
<button
className="btn-text"
style={{ fontSize: 10, padding: '2px 6px' }}
onClick={() => setSection({ hatchColor: null })}
>×</button>
)}
</Field>
{secHatchPat && (
<>
<Field label="HATCH SKALIERUNG">
<input
type="number" step="0.05" min="0.001"
value={sec.hatchScale}
onChange={(ev) => setSection({ hatchScale: parseFloat(ev.target.value) || 1.0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
<Field label="HATCH DREHUNG (°)">
<input
type="number" step="5"
value={sec.hatchRotation}
onChange={(ev) => setSection({ hatchRotation: parseFloat(ev.target.value) || 0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
</>
)}
<Field label="HINTERGRUND">
<select
value={sec.background}
onChange={(ev) => setSection({ background: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value="viewport">Viewport (transparent)</option>
<option value="object">By Object (Layer-Farbe)</option>
</select>
</Field>
</>
)}
{/* --- Boundary --- */}
<div style={{ marginTop: 8, padding: '4px 0',
borderTop: '1px dashed var(--border-light)' }}>
<span style={{ fontSize: 9, color: 'var(--text-muted)',
letterSpacing: 0.4, textTransform: 'uppercase' }}>
Boundary (Schnittkante)
</span>
</div>
<Field label="ZEIGE BOUNDARY">
<input
type="checkbox"
checked={sec.boundaryShow}
onChange={(ev) => setSection({ boundaryShow: ev.target.checked })}
style={{ marginRight: 6 }}
/>
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
{sec.boundaryShow ? 'Schnittkante wird gezeichnet' : 'Schnittkante unsichtbar'}
</span>
</Field>
{sec.boundaryShow && (
<>
<Field label="BOUNDARY FARBE" hint="Leer = Stift der Ebene (By Object)">
<input
type="color"
value={secBoundColor}
onChange={(ev) => setSection({ boundaryColor: ev.target.value })}
style={{
width: 32, height: 22, padding: 0,
border: '1px solid var(--border)', borderRadius: 'var(--r)',
cursor: 'pointer', background: 'transparent',
}}
/>
<span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-muted)', flex: 1 }}>
{sec.boundaryColor || 'By Object'}
</span>
{sec.boundaryColor && (
<button
className="btn-text"
style={{ fontSize: 10, padding: '2px 6px' }}
onClick={() => setSection({ boundaryColor: null })}
>×</button>
)}
</Field>
<Field
label="BOUNDARY WIDTH SCALE"
hint="Multiplikator auf die Ebenen-Stiftstärke. 1 = wie Ebene, 3 = 3× dicker."
>
<input
type="number" step="0.5" min="0.1" max="20"
value={sec.boundaryWidthScale}
onChange={(ev) => setSection({ boundaryWidthScale: parseFloat(ev.target.value) || 1.0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
</>
)}
<Field label="OFFENE OBJEKTE">
<input
type="checkbox"
checked={sec.sectionOpenObjects}
onChange={(ev) => setSection({ sectionOpenObjects: ev.target.checked })}
style={{ marginRight: 6 }}
/>
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
Auch nicht-geschlossene Geometrie schneiden
</span>
</Field>
</div>
{/* Footer */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 12px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-section)',
}}>
<div style={{ flex: 1 }} />
<button className="btn-text" onClick={onClose}>Abbrechen</button>
<button className="btn-contained" onClick={() => onSave(draft)}>Übernehmen</button>
</div>
</div>
</div>
)
}