Satelliten-Dialoge: embedded-Mode + GeschossDialog auch als echtes Fenster

Zwei Dinge:

1. embedded-Mode in den Dialog-Komponenten — wenn TRUE, kein Backdrop +
   keine MaxWidth-Constraint, das Dialog fuellt das ganze WebView statt
   wie ein kleines zentriertes Fenster IN dem WebView gerendert zu werden
   (= "Fenster im Fenster"-Effekt). Betroffen:
   - GeschossSettingsDialog
   - EbenenSettingsDialog
   - GeschossDialog
   Satelliten-Apps (GeschossSettingsApp, EbenenSettingsApp,
   GeschossDialogApp) passen jetzt `embedded` durch.

2. GeschossDialog (= der grosse Mehrfach-Editor hinter dem Pencil-Button)
   laeuft jetzt auch als Satelliten-Fenster — selbe Architektur wie die
   Settings-Dialoge. Backend hat neuen Handler _open_geschoss_dialog und
   neuen Message OPEN_GESCHOSS_DIALOG. Auf Save: ganze z-Liste replace
   + _apply(save_z=True).

GeschossManager braucht den inline-Dialog-State nicht mehr; Pencil-Button
ruft openGeschossDialog(zeichnungsebenen).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 01:30:28 +02:00
parent 1ba0bda429
commit b1b2090b3e
10 changed files with 184 additions and 94 deletions
+23
View File
@@ -221,6 +221,8 @@ class EbenenBridge(panel_base.BaseBridge):
elif t == "OPEN_EBENEN_SETTINGS": elif t == "OPEN_EBENEN_SETTINGS":
self._open_ebenen_settings(p.get("ebene") or {}, self._open_ebenen_settings(p.get("ebene") or {},
p.get("hatchPatterns") or []) p.get("hatchPatterns") or [])
elif t == "OPEN_GESCHOSS_DIALOG":
self._open_geschoss_dialog(p.get("zeichnungsebenen") or [])
# ---- Helpers ---- # ---- Helpers ----
@@ -299,6 +301,27 @@ class EbenenBridge(panel_base.BaseBridge):
size=(420, 600), size=(420, 600),
on_save=on_save) on_save=on_save)
def _open_geschoss_dialog(self, zeichnungsebenen):
"""Oeffnet den vollen GeschossDialog (Mehrfach-Editor) als
Satelliten-Fenster. Save schreibt die ganze z-Liste neu."""
if not isinstance(zeichnungsebenen, list):
print("[EBENEN] open_geschoss_dialog: keine Liste"); return
def on_save(payload):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
new_z = payload.get("zeichnungsebenen") or []
if not new_z: return
e_raw = doc.Strings.GetValue("dossier_ebenen")
try: e_list = json.loads(e_raw) if e_raw else []
except Exception: e_list = []
self._apply(new_z, e_list, save_z=True, save_e=False)
panel_base.open_satellite_window(
"geschoss_dialog",
params={"zeichnungsebenen": zeichnungsebenen},
title="Zeichnungsebenen bearbeiten",
size=(560, 620),
on_save=on_save)
def _apply(self, zeichnungsebenen, ebenen, save_z=True, save_e=True): def _apply(self, zeichnungsebenen, ebenen, save_z=True, save_e=True):
print("[EBENEN] _apply START z={} e={} (save_z={} save_e={})".format( print("[EBENEN] _apply START z={} e={} (save_z={} save_e={})".format(
len(zeichnungsebenen) if zeichnungsebenen else 0, len(zeichnungsebenen) if zeichnungsebenen else 0,
+1 -6
View File
@@ -26,17 +26,12 @@ export default function EbenenSettingsApp() {
} }
return ( return (
<div style={{
width: '100vw', height: '100vh',
background: 'var(--bg-base)',
overflow: 'hidden',
}}>
<EbenenSettingsDialog <EbenenSettingsDialog
embedded
ebene={ebene} ebene={ebene}
hatchPatterns={hatchPatterns} hatchPatterns={hatchPatterns}
onSave={(updated) => bridgeSend('SAVE', updated)} onSave={(updated) => bridgeSend('SAVE', updated)}
onClose={() => bridgeSend('CANCEL', {})} onClose={() => bridgeSend('CANCEL', {})}
/> />
</div>
) )
} }
+49
View File
@@ -0,0 +1,49 @@
import { useEffect } from 'react'
import GeschossDialog from './components/GeschossDialog'
import { notifyReady } from './lib/rhinoBridge'
function bridgeSend(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
const json = JSON.stringify({ type, payload })
document.title = 'RHINOMSG::' + json
}
// recalcOkff direkt hier — gleiche Logik wie in ZeichnungsebenenApp.jsx,
// damit der Dialog die OKFF-Werte beim Editieren live updaten kann.
function recalcOkff(list) {
let acc = 0
return list.map(z => {
if (z.isGeschoss) {
const next = { ...z, okff: parseFloat(acc.toFixed(3)) }
acc += (z.hoehe ?? 3.0)
return next
}
return { ...z, okff: undefined }
})
}
export default function GeschossDialogApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
useEffect(() => {
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
const z = initial.zeichnungsebenen || initial
if (!Array.isArray(z) || z.length === 0) {
return <div style={{ padding: 20, color: 'var(--text-muted)' }}>Keine Daten</div>
}
return (
<GeschossDialog
embedded
zeichnungsebenen={z}
recalcOkff={recalcOkff}
onSave={(updated) => bridgeSend('SAVE', { zeichnungsebenen: updated })}
onClose={() => bridgeSend('CANCEL', {})}
/>
)
}
+2 -12
View File
@@ -1,9 +1,7 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import GeschossSettingsDialog from './components/GeschossSettingsDialog' import GeschossSettingsDialog from './components/GeschossSettingsDialog'
import { onMessage, notifyReady } from './lib/rhinoBridge' import { notifyReady } from './lib/rhinoBridge'
// Inline send fuer SAVE/CANCEL — schickt direkt an Python-Bridge des
// Satelliten-Fensters.
function bridgeSend(type, payload = {}) { function bridgeSend(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return } if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
const json = JSON.stringify({ type, payload }) const json = JSON.stringify({ type, payload })
@@ -11,33 +9,25 @@ function bridgeSend(type, payload = {}) {
} }
export default function GeschossSettingsApp() { export default function GeschossSettingsApp() {
// PANEL_PARAMS wurden von Python beim Window-Open injiziert.
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {} const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
useEffect(() => { useEffect(() => {
notifyReady() notifyReady()
// Native Browser-Context-Menu unterdruecken
const blockContext = (ev) => ev.preventDefault() const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext) document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext) return () => document.removeEventListener('contextmenu', blockContext)
}, []) }, [])
// Wenn keine Daten da sind: leeres Frame
if (!initial || typeof initial !== 'object') { if (!initial || typeof initial !== 'object') {
return <div style={{ padding: 20, color: 'var(--text-muted)' }}>Keine Daten</div> return <div style={{ padding: 20, color: 'var(--text-muted)' }}>Keine Daten</div>
} }
return ( return (
<div style={{
width: '100vw', height: '100vh',
background: 'var(--bg-base)',
overflow: 'hidden',
}}>
<GeschossSettingsDialog <GeschossSettingsDialog
embedded
geschoss={initial} geschoss={initial}
onSave={(updated) => bridgeSend('SAVE', updated)} onSave={(updated) => bridgeSend('SAVE', updated)}
onClose={() => bridgeSend('CANCEL', {})} onClose={() => bridgeSend('CANCEL', {})}
/> />
</div>
) )
} }
+18 -6
View File
@@ -27,7 +27,7 @@ function SectionLabel({ children }) {
) )
} }
export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'], onSave, onClose }) { export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false }) {
const [draft, setDraft] = useState({ const [draft, setDraft] = useState({
...ebene, ...ebene,
fill: { fill: {
@@ -78,14 +78,22 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
...hatchPatterns.filter(p => p !== 'Solid' && p !== 'None'), ...hatchPatterns.filter(p => p !== 'Solid' && p !== 'None'),
] ]
return ( const wrapperStyle = embedded ? {
<div style={{ width: '100%', height: '100%',
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
position: 'absolute', inset: 0, zIndex: 150, position: 'absolute', inset: 0, zIndex: 150,
background: 'var(--bg-overlay)', background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center', display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 30, paddingTop: 30,
}}> }
<div style={{ const innerStyle = embedded ? {
width: '100%', height: '100%',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
background: 'var(--bg-dialog)', background: 'var(--bg-dialog)',
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)', borderRadius: 'var(--r-lg)',
@@ -94,7 +102,11 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
overflow: 'hidden', overflow: 'hidden',
maxHeight: 'calc(100vh - 60px)', maxHeight: 'calc(100vh - 60px)',
}}> }
return (
<div style={wrapperStyle}>
<div style={innerStyle}>
{/* Header */} {/* Header */}
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 6, display: 'flex', alignItems: 'center', gap: 6,
+17 -6
View File
@@ -1,7 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import Icon from './Icon' import Icon from './Icon'
export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, onClose }) { export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, onClose, embedded = false }) {
const [draft, setDraft] = useState(zeichnungsebenen.map(z => ({ ...z }))) const [draft, setDraft] = useState(zeichnungsebenen.map(z => ({ ...z })))
const update = (i, field, val) => { const update = (i, field, val) => {
@@ -56,14 +56,22 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
del: { width: 22, flexShrink: 0 }, del: { width: 22, flexShrink: 0 },
} }
return ( const wrapperStyle = embedded ? {
<div style={{ width: '100%', height: '100%',
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
position: 'absolute', inset: 0, zIndex: 100, position: 'absolute', inset: 0, zIndex: 100,
background: 'var(--bg-overlay)', background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center', display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 40, paddingTop: 40,
}}> }
<div style={{ const innerStyle = embedded ? {
width: '100%', height: '100%',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
background: 'var(--bg-dialog)', background: 'var(--bg-dialog)',
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)', borderRadius: 'var(--r-lg)',
@@ -72,7 +80,10 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
maxHeight: 'calc(100vh - 80px)', maxHeight: 'calc(100vh - 80px)',
overflow: 'hidden', overflow: 'hidden',
}}> }
return (
<div style={wrapperStyle}>
<div style={innerStyle}>
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 8, display: 'flex', alignItems: 'center', gap: 8,
padding: '10px 14px', borderBottom: '1px solid var(--border)', flexShrink: 0, padding: '10px 14px', borderBottom: '1px solid var(--border)', flexShrink: 0,
+4 -14
View File
@@ -1,7 +1,5 @@
import { useState } from 'react'
import Icon from './Icon' import Icon from './Icon'
import GeschossDialog from './GeschossDialog' import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge'
import { openGeschossSettings } from '../lib/rhinoBridge'
function GeschossBadge({ name }) { function GeschossBadge({ name }) {
return <span className="chip chip-info">{name}</span> return <span className="chip chip-info">{name}</span>
@@ -85,7 +83,8 @@ export default function GeschossManager({
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff, zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
mode, onModeChange, mode, onModeChange,
}) { }) {
const [dialogOpen, setDialogOpen] = useState(false) // dialogOpen-State entfaellt — Bearbeiten-Dialog laeuft jetzt als
// Satelliten-Fenster via openGeschossDialog().
const sorted = [...zeichnungsebenen].reverse() const sorted = [...zeichnungsebenen].reverse()
const gesamthoehe = zeichnungsebenen const gesamthoehe = zeichnungsebenen
@@ -134,7 +133,7 @@ export default function GeschossManager({
<button className="btn-icon-sm" onClick={addQuick} title="Zeichnungsebene hinzufügen"> <button className="btn-icon-sm" onClick={addQuick} title="Zeichnungsebene hinzufügen">
<Icon name="add" size={14} /> <Icon name="add" size={14} />
</button> </button>
<button className="btn-icon-sm" onClick={() => setDialogOpen(true)} title="Bearbeiten"> <button className="btn-icon-sm" onClick={() => openGeschossDialog(zeichnungsebenen)} title="Bearbeiten">
<Icon name="edit" size={13} /> <Icon name="edit" size={13} />
</button> </button>
</div> </div>
@@ -166,15 +165,6 @@ export default function GeschossManager({
))} ))}
</div> </div>
{dialogOpen && (
<GeschossDialog
zeichnungsebenen={zeichnungsebenen}
recalcOkff={recalcOkff}
onSave={(updated) => { onChange(updated); setDialogOpen(false) }}
onClose={() => setDialogOpen(false)}
/>
)}
</> </>
) )
} }
+22 -7
View File
@@ -35,7 +35,7 @@ function Toggle({ label, checked, onChange, hint }) {
) )
} }
export default function GeschossSettingsDialog({ geschoss, onSave, onClose }) { export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embedded = false }) {
const [draft, setDraft] = useState({ ...geschoss }) const [draft, setDraft] = useState({ ...geschoss })
const set = (patch) => setDraft({ ...draft, ...patch }) const set = (patch) => setDraft({ ...draft, ...patch })
@@ -46,14 +46,25 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose }) {
const okff = draft.okff ?? 0 const okff = draft.okff ?? 0
const clipZ = (okff + schnitt).toFixed(2) const clipZ = (okff + schnitt).toFixed(2)
return ( // embedded=true: in einem Satelliten-Fenster gerendert — kein Backdrop,
<div style={{ // 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, position: 'absolute', inset: 0, zIndex: 150,
background: 'var(--bg-overlay)', background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center', display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 30, paddingTop: 30,
}}> }
<div style={{ const innerStyle = embedded ? {
width: '100%', height: '100%',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
background: 'var(--bg-dialog)', background: 'var(--bg-dialog)',
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)', borderRadius: 'var(--r-lg)',
@@ -61,7 +72,11 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose }) {
width: 'calc(100% - 16px)', maxWidth: 280, width: 'calc(100% - 16px)', maxWidth: 280,
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
overflow: 'hidden', overflow: 'hidden',
}}> }
return (
<Wrapper style={wrapperStyle}>
<div style={innerStyle}>
{/* Header */} {/* Header */}
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 6, display: 'flex', alignItems: 'center', gap: 6,
@@ -155,6 +170,6 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose }) {
}}>Übernehmen</button> }}>Übernehmen</button>
</div> </div>
</div> </div>
</div> </Wrapper>
) )
} }
+3
View File
@@ -285,6 +285,9 @@ export function openGeschossSettings(geschoss) {
export function openEbenenSettings(ebene, hatchPatterns) { export function openEbenenSettings(ebene, hatchPatterns) {
send('OPEN_EBENEN_SETTINGS', { ebene, hatchPatterns }) send('OPEN_EBENEN_SETTINGS', { ebene, hatchPatterns })
} }
export function openGeschossDialog(zeichnungsebenen) {
send('OPEN_GESCHOSS_DIALOG', { zeichnungsebenen })
}
export function applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, zMode, eMode) { export function applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, zMode, eMode) {
// Split-Panels koennen mit null/[] fuer fremde Slice aufrufen — Backend // Split-Panels koennen mit null/[] fuer fremde Slice aufrufen — Backend
+2
View File
@@ -5,6 +5,7 @@ import App from './App.jsx'
import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx' import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx'
import GeschossSettingsApp from './GeschossSettingsApp.jsx' import GeschossSettingsApp from './GeschossSettingsApp.jsx'
import EbenenSettingsApp from './EbenenSettingsApp.jsx' import EbenenSettingsApp from './EbenenSettingsApp.jsx'
import GeschossDialogApp from './GeschossDialogApp.jsx'
import GestaltungApp from './GestaltungApp.jsx' import GestaltungApp from './GestaltungApp.jsx'
import AusschnitteApp from './AusschnitteApp.jsx' import AusschnitteApp from './AusschnitteApp.jsx'
import MassstabApp from './MassstabApp.jsx' import MassstabApp from './MassstabApp.jsx'
@@ -28,6 +29,7 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
: mode === 'zeichnungsebenen' ? ZeichnungsebenenApp : mode === 'zeichnungsebenen' ? ZeichnungsebenenApp
: mode === 'geschoss_settings' ? GeschossSettingsApp : mode === 'geschoss_settings' ? GeschossSettingsApp
: mode === 'ebenen_settings' ? EbenenSettingsApp : mode === 'ebenen_settings' ? EbenenSettingsApp
: mode === 'geschoss_dialog' ? GeschossDialogApp
: App : App
window.onerror = function (msg, src, line, col, err) { window.onerror = function (msg, src, line, col, err) {