95031ee2c0
- 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>
468 lines
18 KiB
React
468 lines
18 KiB
React
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>
|
||
)
|
||
}
|