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>
This commit is contained in:
@@ -76,6 +76,11 @@ export default function SwisstopoApp() {
|
||||
const [replaceExisting, setReplaceExisting] = useState(true)
|
||||
const [clipToBbox, setClipToBbox] = useState(false)
|
||||
const [terrainRes, setTerrainRes] = useState('2.0')
|
||||
// Terrain als geschlossenes Volumen (mit Boden 10m unter tiefstem Punkt)
|
||||
// damit Section-Cuts gefuellte Querschnitte zeigen statt nur Linien.
|
||||
// Wirkt auf 3D-Mesh / TIN / Patch — nicht auf 2D-Hoehenlinien und Schichten.
|
||||
const [terrainVolume, setTerrainVolume] = useState(false)
|
||||
const [terrainVolumeDepth, setTerrainVolumeDepth] = useState('10')
|
||||
// Live-Log
|
||||
const [logs, setLogs] = useState([])
|
||||
const [running, setRunning] = useState(false)
|
||||
@@ -165,6 +170,8 @@ export default function SwisstopoApp() {
|
||||
buildVariant,
|
||||
contourInterval: contourInt,
|
||||
tlmKinds: tlmList,
|
||||
terrainAsVolume: terrainVolume,
|
||||
terrainVolumeDepth: parseFloat(terrainVolumeDepth) || 10,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -366,6 +373,31 @@ export default function SwisstopoApp() {
|
||||
onChange={setContourInt} />
|
||||
</Field>
|
||||
)}
|
||||
{(getTerrain || getContourTin || getContourPatch) && (
|
||||
<>
|
||||
<SectionLabel>Nachbearbeitung</SectionLabel>
|
||||
<Field label=""
|
||||
hint="Wandelt die oben gewaehlten 3D-Terrain-Quellen (Terrain / TIN / Patch) in geschlossene Mesh-Volumen um — Skirt + Boden bei (min_z − Tiefe). Damit liefert eine Clipping-Plane einen gefuellten Querschnitt statt nur Konturlinien. 2D-Linien und Schichten sind nicht betroffen.">
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
||||
fontSize: 11, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={terrainVolume}
|
||||
onChange={(e) => setTerrainVolume(e.target.checked)} />
|
||||
<Icon name="layers" size={13} /> Terrain als Volumen (mit Boden, schneidbar)
|
||||
</label>
|
||||
</Field>
|
||||
{terrainVolume && (
|
||||
<Field label="TIEFE">
|
||||
<input type="text"
|
||||
value={terrainVolumeDepth}
|
||||
onChange={(e) => setTerrainVolumeDepth(e.target.value)}
|
||||
style={{ width: 60, textAlign: 'right' }} />
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
m unter tiefstem Punkt
|
||||
</span>
|
||||
</Field>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<SectionLabel>Positionierung</SectionLabel>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import ContextMenu from './ContextMenu'
|
||||
import { BarCombo, BarButton } from './BarControls'
|
||||
import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge'
|
||||
import { openGeschossSettings, openGeschossDialog, createSchnitt } from '../lib/rhinoBridge'
|
||||
|
||||
function GeschossBadge({ name }) {
|
||||
return <span className="chip chip-info">{name}</span>
|
||||
@@ -13,6 +13,10 @@ function ZeichnungsebeneRow({
|
||||
onToggleVisible, onToggleLock, onToggleClipping, onDelete,
|
||||
}) {
|
||||
const isGeschoss = !!z.isGeschoss
|
||||
const isSchnitt = z.type === 'schnitt'
|
||||
// Schnitt vs Ansicht: cutAtLine!=false = Schnitt (mit Front-Cut), sonst Ansicht
|
||||
const schnittIcon = (z.cutAtLine === false) ? 'visibility' : 'content_cut'
|
||||
const schnittLabel = (z.cutAtLine === false) ? 'Ansicht' : 'Schnitt'
|
||||
// Eye-Logik: die aktive Z ist IMMER sichtbar (Backend forciert das), also
|
||||
// zeigen wir ihr Auge immer als "an" — ohne Ruecksicht aufs visible-Flag.
|
||||
// Nicht-aktive: in 'all_force' ist visible-Flag ueberschrieben (alle an),
|
||||
@@ -85,6 +89,18 @@ function ZeichnungsebeneRow({
|
||||
|
||||
{isGeschoss && <GeschossBadge name={z.name} />}
|
||||
|
||||
{isSchnitt && (
|
||||
<span title={schnittLabel}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
fontSize: 10, color: 'var(--text-muted)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
}}>
|
||||
<Icon name={schnittIcon} size={11}
|
||||
style={{ color: active ? 'var(--accent)' : 'var(--text-muted)' }} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isGeschoss ? (
|
||||
<button
|
||||
className={`btn-icon-xs ${z.hasClipping ? 'is-on' : ''}`}
|
||||
@@ -128,14 +144,14 @@ export default function GeschossManager({
|
||||
mode, onModeChange,
|
||||
}) {
|
||||
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
||||
const [addMenu, setAddMenu] = useState(null) // { x, y } — Picker beim +
|
||||
const [geschossDialog, setGeschossDialog] = useState(null) // { x, y, pos:'above'|'below', name, hoehe, schnitthoehe, anchorId }
|
||||
|
||||
const sorted = [...zeichnungsebenen].reverse()
|
||||
|
||||
const addQuick = () => {
|
||||
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
|
||||
// Plangrafik etc.). User kann via Row-Kontextmenue auf Geschoss
|
||||
// umschalten oder via Bearbeiten-Dialog (Pencil) ein Geschoss erstellen.
|
||||
const nonGeschossCount = zeichnungsebenen.filter(z => !z.isGeschoss).length
|
||||
const addZeichnung = () => {
|
||||
const nonGeschossCount = zeichnungsebenen.filter(
|
||||
z => !z.isGeschoss && z.type !== "schnitt").length
|
||||
const newZ = {
|
||||
id: `z_${Date.now()}`,
|
||||
name: `Zeichnung ${nonGeschossCount + 1}`,
|
||||
@@ -145,6 +161,151 @@ export default function GeschossManager({
|
||||
onChange([...zeichnungsebenen, newZ])
|
||||
}
|
||||
|
||||
// Vorgeschlagener Geschossname relativ zum Anker. Logik:
|
||||
// EG + above → 1OG, EG + below → UG
|
||||
// <N>OG + above → <N+1>OG, <N>OG + below → wenn N=1 dann EG, sonst <N-1>OG
|
||||
// <N>UG + above → wenn N=1 dann EG, sonst <N-1>UG; <N>UG + below → <N+1>UG
|
||||
// sonst: 'Neu'
|
||||
const suggestGeschossName = (anchor, pos) => {
|
||||
const nm = ((anchor?.name) || '').trim().toUpperCase()
|
||||
const ogMatch = nm.match(/^(\d*)OG$/)
|
||||
const ugMatch = nm.match(/^(\d*)UG$/)
|
||||
if (nm === 'EG') return pos === 'above' ? '1OG' : 'UG'
|
||||
if (ogMatch) {
|
||||
const n = parseInt(ogMatch[1] || '1', 10)
|
||||
if (pos === 'above') return `${n + 1}OG`
|
||||
return n <= 1 ? 'EG' : `${n - 1}OG`
|
||||
}
|
||||
if (ugMatch) {
|
||||
const n = parseInt(ugMatch[1] || '1', 10)
|
||||
if (pos === 'below') return `${n + 1}UG`
|
||||
return n <= 1 ? 'EG' : `${n - 1}UG`
|
||||
}
|
||||
return 'Neu'
|
||||
}
|
||||
|
||||
// Project-Default-Hoehe: erst aktives Geschoss, dann erstes Geschoss in
|
||||
// der Liste, dann hartcodiert 3.0. So uebernimmt jeder neue Eintrag den
|
||||
// "typischen" Wert des Projekts ohne dass der User irgendwo setzen muss.
|
||||
const defaultGeschossHoehe = () => {
|
||||
const act = zeichnungsebenen.find(z => z.id === activeId)
|
||||
if (act?.isGeschoss && act.hoehe != null) return act.hoehe
|
||||
const first = zeichnungsebenen.find(z => z.isGeschoss && z.hoehe != null)
|
||||
return first?.hoehe ?? 3.0
|
||||
}
|
||||
const defaultSchnitthoehe = () => {
|
||||
const act = zeichnungsebenen.find(z => z.id === activeId)
|
||||
if (act?.isGeschoss && act.schnitthoehe != null) return act.schnitthoehe
|
||||
const first = zeichnungsebenen.find(z => z.isGeschoss && z.schnitthoehe != null)
|
||||
return first?.schnitthoehe ?? 1.0
|
||||
}
|
||||
|
||||
const openGeschossPrompt = (ev) => {
|
||||
// Anker = aktives Geschoss (oder erstes Geschoss in der Liste). Falls
|
||||
// gar keins da: einfach default-Werte ohne Anker.
|
||||
const anchor = zeichnungsebenen.find(z => z.id === activeId && z.isGeschoss)
|
||||
|| zeichnungsebenen.find(z => z.isGeschoss)
|
||||
|| null
|
||||
const pos = 'above'
|
||||
const rect = ev?.currentTarget?.getBoundingClientRect()
|
||||
setGeschossDialog({
|
||||
x: rect ? rect.right - 240 : 200,
|
||||
y: rect ? rect.bottom + 4 : 100,
|
||||
pos,
|
||||
name: suggestGeschossName(anchor, pos),
|
||||
hoehe: defaultGeschossHoehe().toFixed(2),
|
||||
schnitthoehe: defaultSchnitthoehe().toFixed(2),
|
||||
anchorId: anchor?.id || null,
|
||||
})
|
||||
}
|
||||
|
||||
const confirmGeschoss = () => {
|
||||
if (!geschossDialog) return
|
||||
const { pos, name, hoehe, schnitthoehe, anchorId } = geschossDialog
|
||||
const h = parseFloat(String(hoehe).replace(',', '.'))
|
||||
const sh = parseFloat(String(schnitthoehe).replace(',', '.'))
|
||||
const newZ = {
|
||||
id: `z_${Date.now()}`,
|
||||
name: (name || 'Neu').trim(),
|
||||
isGeschoss: true,
|
||||
hoehe: isFinite(h) && h > 0 ? h : 3.0,
|
||||
schnitthoehe: isFinite(sh) && sh > 0 ? sh : 1.0,
|
||||
visible: true,
|
||||
}
|
||||
// Insertion-Index: anchor finden, dann +1 (above) oder vorne ein (below).
|
||||
// Array-Reihenfolge = bottom-up (recalcOkff stacks von index 0 aufwaerts),
|
||||
// also "above" = nach anchor, "below" = vor anchor.
|
||||
let nextList
|
||||
if (anchorId) {
|
||||
const idx = zeichnungsebenen.findIndex(z => z.id === anchorId)
|
||||
if (idx >= 0) {
|
||||
const insertAt = pos === 'above' ? idx + 1 : idx
|
||||
nextList = [
|
||||
...zeichnungsebenen.slice(0, insertAt),
|
||||
newZ,
|
||||
...zeichnungsebenen.slice(insertAt),
|
||||
]
|
||||
} else {
|
||||
nextList = [...zeichnungsebenen, newZ]
|
||||
}
|
||||
} else {
|
||||
nextList = [...zeichnungsebenen, newZ]
|
||||
}
|
||||
onChange(nextList)
|
||||
setGeschossDialog(null)
|
||||
}
|
||||
|
||||
const updateGeschossDialog = (patch) => {
|
||||
if (!geschossDialog) return
|
||||
const next = { ...geschossDialog, ...patch }
|
||||
// Wenn pos ODER anchorId sich aendert: Name + Hoehen neu vorschlagen,
|
||||
// aber nur wenn der User die jeweiligen Felder nicht schon manuell
|
||||
// ueberschrieben hat (heuristisch: aktueller Wert === alter Vorschlag).
|
||||
const posChanged = patch.pos && patch.pos !== geschossDialog.pos
|
||||
const anchorChanged = patch.anchorId && patch.anchorId !== geschossDialog.anchorId
|
||||
if (posChanged || anchorChanged) {
|
||||
const oldAnchor = zeichnungsebenen.find(z => z.id === geschossDialog.anchorId)
|
||||
const newAnchor = zeichnungsebenen.find(z => z.id === (patch.anchorId || geschossDialog.anchorId))
|
||||
const oldSuggestedName = suggestGeschossName(oldAnchor, geschossDialog.pos)
|
||||
if (geschossDialog.name === oldSuggestedName) {
|
||||
next.name = suggestGeschossName(newAnchor, next.pos)
|
||||
}
|
||||
// Bei Anchor-Wechsel: Hoehe/Schnitthoehe auch nachziehen wenn unveraendert
|
||||
if (anchorChanged && newAnchor?.isGeschoss) {
|
||||
const oldHoeheStr = (oldAnchor?.hoehe ?? 3.0).toFixed(2)
|
||||
const oldShStr = (oldAnchor?.schnitthoehe ?? 1.0).toFixed(2)
|
||||
if (geschossDialog.hoehe === oldHoeheStr && newAnchor.hoehe != null) {
|
||||
next.hoehe = newAnchor.hoehe.toFixed(2)
|
||||
}
|
||||
if (geschossDialog.schnitthoehe === oldShStr && newAnchor.schnitthoehe != null) {
|
||||
next.schnitthoehe = newAnchor.schnitthoehe.toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
setGeschossDialog(next)
|
||||
}
|
||||
|
||||
const addSchnitt = () => createSchnitt() // triggert interaktiven Pick + Auto-Activate
|
||||
|
||||
const openAddMenu = (ev) => {
|
||||
if (!ev) { addZeichnung(); return } // Fallback ohne Event-Position
|
||||
const rect = ev.currentTarget.getBoundingClientRect()
|
||||
setAddMenu({ x: rect.right - 180, y: rect.bottom + 4 })
|
||||
}
|
||||
|
||||
// ContextMenu Items — onClick kriegt KEIN Event. Daher fuer Geschoss
|
||||
// einen kleinen Trick: ContextMenu schliesst, dann oeffnen wir den
|
||||
// Dialog mit einer kuenstlichen Position (rechts vom Panel).
|
||||
const addMenuItems = [
|
||||
{ label: 'Geschoss', icon: 'layers',
|
||||
onClick: () => openGeschossPrompt({
|
||||
currentTarget: { getBoundingClientRect: () =>
|
||||
({ right: addMenu?.x + 180, bottom: addMenu?.y + 20 }) } }) },
|
||||
{ label: 'Schnitt / Ansicht', icon: 'content_cut', onClick: addSchnitt },
|
||||
{ divider: true },
|
||||
{ label: 'Zeichnung', icon: 'edit_note', onClick: addZeichnung },
|
||||
]
|
||||
|
||||
const toggleVisible = (id) => {
|
||||
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z))
|
||||
// In "active" / "all_force" greift visible-Flag nicht — wer aufs Auge
|
||||
@@ -226,9 +387,9 @@ export default function GeschossManager({
|
||||
onGear={() => openGeschossDialog(zeichnungsebenen)}
|
||||
gearIcon="settings"
|
||||
gearTitle="Einstellungen"
|
||||
onSecond={addQuick}
|
||||
onSecond={openAddMenu}
|
||||
secondIcon="add"
|
||||
secondTitle="Zeichnungsebene hinzufügen"
|
||||
secondTitle="Hinzufuegen: Geschoss / Schnitt / Zeichnung"
|
||||
>
|
||||
{MODES.map(m => (
|
||||
<option key={m.value} value={m.value}>{m.label}</option>
|
||||
@@ -309,6 +470,120 @@ export default function GeschossManager({
|
||||
onClose={() => setCtxMenu(null)}
|
||||
/>
|
||||
)}
|
||||
{addMenu && (
|
||||
<ContextMenu
|
||||
x={addMenu.x} y={addMenu.y}
|
||||
items={addMenuItems}
|
||||
onClose={() => setAddMenu(null)}
|
||||
/>
|
||||
)}
|
||||
{geschossDialog && (() => {
|
||||
// Geschoss-Liste (top-down sortiert wie im Panel) fuer das Dropdown
|
||||
const geschossOptions = [...zeichnungsebenen]
|
||||
.filter(z => z.isGeschoss)
|
||||
.reverse()
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop — Klick schliesst */}
|
||||
<div onClick={() => setGeschossDialog(null)}
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 999,
|
||||
background: 'transparent' }} />
|
||||
<div style={{
|
||||
position: 'fixed', zIndex: 1000,
|
||||
left: Math.max(8, Math.min(geschossDialog.x, window.innerWidth - 260)),
|
||||
top: Math.max(8, Math.min(geschossDialog.y, window.innerHeight - 220)),
|
||||
width: 240,
|
||||
background: 'var(--bg-dialog)', color: 'var(--text-primary)',
|
||||
border: '1px solid var(--border)', borderRadius: 6,
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
|
||||
padding: 10,
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
display: 'flex', flexDirection: 'column', gap: 8,
|
||||
}}
|
||||
onClick={(ev) => ev.stopPropagation()}>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||
fontWeight: 600, letterSpacing: 0.4,
|
||||
textTransform: 'uppercase' }}>
|
||||
Neues Geschoss
|
||||
</div>
|
||||
|
||||
{/* Position: Anker-Dropdown + Über/Unter-Toggle */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}>
|
||||
Position relativ zu
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<select
|
||||
value={geschossDialog.anchorId || ''}
|
||||
onChange={(ev) => updateGeschossDialog({ anchorId: ev.target.value })}
|
||||
disabled={geschossOptions.length === 0}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}>
|
||||
{geschossOptions.length === 0 && <option value="">— keins —</option>}
|
||||
{geschossOptions.map(g => (
|
||||
<option key={g.id} value={g.id}>{g.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<BarButton icon="arrow_upward"
|
||||
active={geschossDialog.pos === 'above'}
|
||||
onClick={() => updateGeschossDialog({ pos: 'above' })}
|
||||
title="Über dem Anker einfügen" />
|
||||
<BarButton icon="arrow_downward"
|
||||
active={geschossDialog.pos === 'below'}
|
||||
onClick={() => updateGeschossDialog({ pos: 'below' })}
|
||||
title="Unter dem Anker einfügen" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}>Name</span>
|
||||
<input type="text" value={geschossDialog.name}
|
||||
onChange={(ev) => updateGeschossDialog({ name: ev.target.value })}
|
||||
autoFocus
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') confirmGeschoss()
|
||||
else if (ev.key === 'Escape') setGeschossDialog(null)
|
||||
}}
|
||||
style={{ width: '100%' }} />
|
||||
</div>
|
||||
|
||||
{/* Hoehe + Schnitthoehe */}
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}>Höhe (m)</span>
|
||||
<input type="text" value={geschossDialog.hoehe}
|
||||
onChange={(ev) => updateGeschossDialog({ hoehe: ev.target.value })}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') confirmGeschoss()
|
||||
else if (ev.key === 'Escape') setGeschossDialog(null)
|
||||
}}
|
||||
style={{ width: '100%', textAlign: 'right' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}
|
||||
title="Höhe der horizontalen Schnitt-Plane über OKFF">
|
||||
Schnitt (m)
|
||||
</span>
|
||||
<input type="text" value={geschossDialog.schnitthoehe}
|
||||
onChange={(ev) => updateGeschossDialog({ schnitthoehe: ev.target.value })}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') confirmGeschoss()
|
||||
else if (ev.key === 'Escape') setGeschossDialog(null)
|
||||
}}
|
||||
style={{ width: '100%', textAlign: 'right' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 4, marginTop: 4 }}>
|
||||
<button className="btn-text"
|
||||
onClick={() => setGeschossDialog(null)}>Abbrechen</button>
|
||||
<button className="btn-contained"
|
||||
onClick={confirmGeschoss}>Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,12 +39,19 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
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)
|
||||
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.
|
||||
@@ -103,14 +110,72 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Toggle
|
||||
label="Ist Geschoss"
|
||||
checked={isG}
|
||||
onChange={(v) => set({ isGeschoss: v })}
|
||||
hint={isG ? 'Höhe & Clipping verfügbar' : 'reines Zeichenblatt'}
|
||||
/>
|
||||
{/* 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'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isG && (
|
||||
{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' }} />
|
||||
|
||||
@@ -180,6 +245,13 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
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>
|
||||
|
||||
@@ -175,6 +175,16 @@ export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {})
|
||||
export function setDarstellung(d) { send('SET_DARSTELLUNG', { darstellung: d || '' }) }
|
||||
// Anordnen — 2D-Z-Stack via Rhino-DisplayOrder. dir: 'front'|'forward'|'backward'|'back'
|
||||
export function arrangeSelection(dir) { send('ARRANGE', { dir }) }
|
||||
// Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt
|
||||
// eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie.
|
||||
// opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }
|
||||
export function createSchnitt(opts = {}) {
|
||||
send('CREATE_SCHNITT', {
|
||||
cutAtLine: true, depthBack: 8.0, heightMin: -1.0, heightMax: 12.0,
|
||||
...opts,
|
||||
})
|
||||
}
|
||||
export function deleteSchnitt(id) { send('DELETE_SCHNITT', { id }) }
|
||||
export function saveOeffStyle(name, settings) {
|
||||
send('SAVE_OEFF_STYLE', { name, settings })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user