Files
DOSSIER/src/components/GeschossSettingsDialog.jsx
T
karim 059cbf8d4d Schnitt/Ansicht-Feature + Terrain-Volumen + Geschoss-Add-Dialog
Schnitt-Feature V1+V2:
- Neues rhino/schnitte.py mit Pick-Workflow, Activation (Clipping-Planes +
  Parallel-View), 2D-Plan-Symbol auf 18_Schnittlinien-Sublayer
- Doppelklick auf Symbol aktiviert den Schnitt
- Schnitt-Settings (cutAtLine/Tiefe/Höhen/Blickrichtung) im GeschossSettingsDialog
- View-Snapshot + Restore beim Wechsel Schnitt → Geschoss
- Symbol-Cleanup bei Delete via normalem Ebenen-Menü

Terrain als Volumen:
- swisstopo.volumize_terrain_object: Skirt + Bottom-Cap auf Mesh/Brep
  damit Clipping-Planes gefuellte Querschnitte erzeugen
- UI im SwisstopoApp mit Nachbearbeitung-Section + Tiefen-Eingabe

Geschoss-Add mit Dialog:
- + im GeschossManager oeffnet 3-Optionen-Picker (Geschoss/Schnitt/Zeichnung)
- Geschoss-Dialog mit Anker-Dropdown, Position über/unter, Auto-Name,
  Höhen-Prefill aus Anker

Fix: _send_state fallback — Element gilt als selektiert wenn Source ODER
Volume in der Selection ist (robust gegen Layer-Visibility wenn Referenz-
linien-Layer im aktuellen Mode versteckt ist)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 18:28:59 +02:00

262 lines
10 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'
/** Vertikales Feld-Layout: Label oben, Input darunter — passt in schmale Panels. */
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>
)
}
/** Toggle-Reihe: Checkbox + Label inline, Hint darunter wenn vorhanden. */
function Toggle({ label, checked, onChange, hint }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, padding: '5px 0' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>
<input type="checkbox" checked={checked} onChange={(ev) => onChange(ev.target.checked)} />
<span style={{ fontSize: 11, color: 'var(--text-primary)' }}>{label}</span>
</label>
{hint && (
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4, marginLeft: 22 }}>
{hint}
</span>
)}
</div>
)
}
export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embedded = false }) {
const [draft, setDraft] = useState({ ...geschoss })
const set = (patch) => setDraft({ ...draft, ...patch })
const isG = !!draft.isGeschoss
const isSchnitt = draft.type === 'schnitt'
const hoehe = draft.hoehe ?? 3.0
const schnitt = draft.schnitthoehe ?? 1.0
const hasClip = !!draft.hasClipping
const okff = draft.okff ?? 0
const clipZ = (okff + schnitt).toFixed(2)
// Schnitt-Felder
const cutAtLine = draft.cutAtLine !== false // default true = Schnitt
const depthBack = draft.depthBack ?? 8.0
const heightMin = draft.heightMin ?? -1.0
const heightMax = draft.heightMax ?? 12.0
const dirSign = draft.dirSign ?? 1
// embedded=true: in einem Satelliten-Fenster gerendert — kein Backdrop,
// keine Width-Constraint, fuellt das ganze WebView.
const Wrapper = embedded ? 'div' : 'div'
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: 280,
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
}
return (
<Wrapper style={wrapperStyle}>
<div style={innerStyle}>
{/* Header */}
<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',
}}>
{geschoss.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' }}>
<Field label="NAME">
<input
value={draft.name}
onChange={(ev) => set({ name: ev.target.value })}
style={{ flex: 1, fontSize: 11, fontWeight: 600, minWidth: 0 }}
/>
</Field>
{/* Geschoss-Toggle nur fuer non-schnitt Eintraege — Schnitt-Type
ist exklusiv (kein Geschoss zugleich). */}
{!isSchnitt && (
<Toggle
label="Ist Geschoss"
checked={isG}
onChange={(v) => set({ isGeschoss: v })}
hint={isG ? 'Höhe & Clipping verfügbar' : 'reines Zeichenblatt'}
/>
)}
{isSchnitt && (
<>
<div style={{ height: 1, background: 'var(--border-light)', margin: '6px 0' }} />
<Toggle
label="Front-Cut (Schnitt durchschneiden)"
checked={cutAtLine}
onChange={(v) => set({ cutAtLine: v })}
hint={cutAtLine
? 'Schnitt: alles vor der Schnittlinie wird weggeschnitten'
: 'Ansicht: nur Tiefenbegrenzung hinten, kein Front-Cut'}
/>
<Field label="TIEFE HINTEN (m)"
hint="Wie weit hinter der Schnittlinie noch sichtbar ist">
<input
type="number" step="0.5" min="0.5"
value={depthBack}
onChange={(ev) => set({ depthBack: parseFloat(ev.target.value) || 8.0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
<div style={{ display: 'flex', gap: 6 }}>
<Field label="HÖHE UNTEN (m)">
<input
type="number" step="0.1"
value={heightMin}
onChange={(ev) => set({ heightMin: parseFloat(ev.target.value) })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
<Field label="HÖHE OBEN (m)">
<input
type="number" step="0.1"
value={heightMax}
onChange={(ev) => set({ heightMax: parseFloat(ev.target.value) })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
</div>
<Field label="BLICKRICHTUNG"
hint="Wechselt zwischen den beiden Seiten der Schnittlinie">
<button className={dirSign >= 0 ? 'btn-contained' : 'btn-outlined'}
onClick={() => set({ dirSign: 1 })}
style={{ flex: 1, fontSize: 11 }}> Seite A</button>
<button className={dirSign < 0 ? 'btn-contained' : 'btn-outlined'}
onClick={() => set({ dirSign: -1 })}
style={{ flex: 1, fontSize: 11 }}>Seite B </button>
</Field>
</>
)}
{isG && !isSchnitt && (
<>
<div style={{ height: 1, background: 'var(--border-light)', margin: '6px 0' }} />
<Field label="HÖHE (m)">
<input
type="number" step="0.05" min="0.5" max="30"
value={hoehe}
onChange={(ev) => set({ hoehe: parseFloat(ev.target.value) || hoehe })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
<Field label="SCHNITTHÖHE (m)" hint="über Geschossboden">
<input
type="number" step="0.05" min="0.1"
value={schnitt}
onChange={(ev) => set({ schnitthoehe: parseFloat(ev.target.value) || 1.0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
/>
</Field>
<div style={{ height: 1, background: 'var(--border-light)', margin: '6px 0' }} />
<Toggle
label="Clipping Plane"
checked={hasClip}
onChange={(v) => set({ hasClipping: v })}
hint={
hasClip
? `Horizontaler Schnitt bei +${clipZ}m (OKFF + Schnitthöhe). Sichtbar in der Top-Ansicht wenn dieses Geschoss aktiv ist.`
: 'aus'
}
/>
</>
)}
<div style={{ height: 1, background: 'var(--border-light)', margin: '6px 0' }} />
<Field
label="0-KOTE m.ü.M (PROJEKTWEIT)"
hint="Höhe ü. Meer am OKFF=0. Wird beim Swisstopo-Import als Z-Offset benutzt — alle Real-Welt-Höhen werden um diesen Wert runtergeschoben. Gilt projektweit (nicht nur dieses Geschoss).">
<input
type="number" step="0.01"
value={draft.projectZeroMum ?? 0}
onChange={(ev) => set({ projectZeroMum: parseFloat(ev.target.value) || 0 })}
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0,
fontFamily: 'var(--font-mono)' }}
/>
</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={() => {
// Numerische Felder NIEMALS als undefined/null rausgehen lassen —
// sonst crasht der Plugin spaeter beim float()-Cast. Defaults
// entsprechen den Werten die das UI auch ohne User-Input zeigt.
const out = { ...draft }
if (out.isGeschoss) {
if (out.hoehe == null) out.hoehe = 3.0
if (out.schnitthoehe == null) out.schnitthoehe = 1.0
}
if (out.type === 'schnitt') {
if (out.depthBack == null) out.depthBack = 8.0
if (out.heightMin == null) out.heightMin = -1.0
if (out.heightMax == null) out.heightMax = 12.0
if (out.dirSign == null) out.dirSign = 1
if (out.cutAtLine == null) out.cutAtLine = true
}
onSave(out)
}}>Übernehmen</button>
</div>
</div>
</Wrapper>
)
}