Files
DOSSIER/src/components/GeschossSettingsDialog.jsx
T
karim afb59b6626 Swisstopo Iter 2 + hierarchische Ebenen + 0-Kote m.ü.M
Swisstopo
- swissBUILDINGS3D 3.0 + Variant-Toggle (separated/solid) im Dialog
- Auto-Fallback auf 2.0 wenn 3.0-Tiles ueber 200 MB sind (Stadt-Fall)
- Defensiver Variant-Filter auf 3 Ebenen (Item, Asset, ZIP-Extract) — keine
  Doppelimporte mehr
- Auto-Skala korrigiert jetzt die importierten Objekte (×1000) statt die
  User-bbox zu schrumpfen — Buildings bleiben in m-Doc-Skala
- merge_grids: XYZ-Tiles werden vor dem Mesh-Bau vereint, kein 1m-Streifen
  zwischen Tiles mehr
- Layer-Konsolidierung: Build_*/Roof_*/Wall_*/Floor_* DWG-Source-Layer
  werden auf Sub-Sub-Layer unter 81_Swissbuildings/{Build,Roof,Wall,Floor}
  gemappt; solid-Variante landet flach direkt auf dem Parent
- 0-Kote m.ü.M (Projekt-Nullpunkt) wird beim Import als Z-Offset angewandt

Hierarchische Ebenen
- dossier_ebenen unterstuetzt jetzt 'children'-Array (rekursiv)
- layer_builder.build_layers rekursiv (Parent + Children unter jedem Geschoss)
- apply_visibility/update_layer_style/set_ebene_visible/set_ebene_locked
  walken den Tree (Sub-Sub-Layer mit gleichem Code-Prefix werden mit-gepflegt)
- EbenenManager mit Chevron-Toggle + Indent pro Level + Context-Menue-Item
  'Sub-Ebene hinzufuegen'
- rhinoBridge.applyVisibility schickt Children-Tree (nicht nur Top-Level) —
  sonst kommen Sub-Toggles nicht beim Backend an
- Visibility-Key in App.jsx rekursiv durch Children — useEffect feuert jetzt
  auch bei Sub-Eye-Toggles

0-Kote m.ü.M
- Eingabefeld im Geschoss-Settings-Dialog (projektweit)
- Speicherung als dossier_project_zero_mum in doc.Strings
- Wird im Swisstopo-Import als Z-Offset (m + doc-units) angewandt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 23:21:45 +02:00

190 lines
7.1 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 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)
// 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>
<Toggle
label="Ist Geschoss"
checked={isG}
onChange={(v) => set({ isGeschoss: v })}
hint={isG ? 'Höhe & Clipping verfügbar' : 'reines Zeichenblatt'}
/>
{isG && (
<>
<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
}
onSave(out)
}}>Übernehmen</button>
</div>
</div>
</Wrapper>
)
}