Panels poliert: Ebenenkombi in Oberleiste, Satelliten-Dialoge, Caps weg, Perf
- 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>
This commit is contained in:
+19
-112
@@ -1,33 +1,29 @@
|
||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import EbenenManager from './components/EbenenManager'
|
||||
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
|
||||
import {
|
||||
applyAll, setActiveEbene,
|
||||
onMessage, notifyReady, applyVisibility,
|
||||
getCombination, applyCombination,
|
||||
saveCurrentAsCombination, deleteCombinationPreset,
|
||||
saveCombinationPreset,
|
||||
} from './lib/rhinoBridge'
|
||||
|
||||
const INITIAL_EBENEN = [
|
||||
{ code: '00', name: 'RASTER', color: '#484850', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '01', name: 'VERMESSUNG', color: '#707078', lw: 0.18, visible: true, locked: false },
|
||||
{ code: '10', name: 'SITUATION', color: '#909090', lw: 0.18, visible: true, locked: false },
|
||||
{ code: '11', name: 'STRASSE', color: '#a89070', lw: 0.18, visible: true, locked: false },
|
||||
{ code: '12', name: 'GEBÄUDE', color: '#888888', lw: 0.25, visible: true, locked: false },
|
||||
{ code: '13', name: 'BÄUME', color: '#50a050', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '14', name: 'HÖHENLINIEN', color: '#909050', lw: 0.18, visible: true, locked: false },
|
||||
{ code: '20', name: 'WÄNDE', color: '#0a0a0a', lw: 0.50, visible: true, locked: false },
|
||||
{ code: '21', name: 'TÜREN_FENSTER', color: '#5080c8', lw: 0.25, visible: true, locked: false },
|
||||
{ code: '22', name: 'MÖBEL', color: '#909090', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '25', name: 'STÜTZEN', color: '#c87050', lw: 0.50, visible: true, locked: false },
|
||||
{ code: '30', name: 'DECKEN', color: '#605850', lw: 0.35, visible: true, locked: false },
|
||||
{ code: '31', name: 'DÄCHER', color: '#7a4a3a', lw: 0.35, visible: true, locked: false },
|
||||
{ code: '35', name: 'TRÄGER', color: '#a87858', lw: 0.50, visible: true, locked: false },
|
||||
{ code: '50', name: 'TEXT', color: '#d0d0d0', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '60', name: 'PLANGRAFIK', color: '#c0a040', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '90', name: 'REFERENZEN', color: '#585860', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '99', name: 'KONSTRUKTION', color: '#404048', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '00', name: 'Raster', color: '#484850', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '01', name: 'Vermessung', color: '#707078', lw: 0.18, visible: true, locked: false },
|
||||
{ code: '10', name: 'Situation', color: '#909090', lw: 0.18, visible: true, locked: false },
|
||||
{ code: '11', name: 'Strasse', color: '#a89070', lw: 0.18, visible: true, locked: false },
|
||||
{ code: '12', name: 'Gebäude', color: '#888888', lw: 0.25, visible: true, locked: false },
|
||||
{ code: '13', name: 'Bäume', color: '#50a050', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '14', name: 'Höhenlinien', color: '#909050', lw: 0.18, visible: true, locked: false },
|
||||
{ code: '20', name: 'Wände', color: '#0a0a0a', lw: 0.50, visible: true, locked: false },
|
||||
{ code: '21', name: 'Türen_Fenster', color: '#5080c8', lw: 0.25, visible: true, locked: false },
|
||||
{ code: '22', name: 'Möbel', color: '#909090', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '25', name: 'Stützen', color: '#c87050', lw: 0.50, visible: true, locked: false },
|
||||
{ code: '30', name: 'Decken', color: '#605850', lw: 0.35, visible: true, locked: false },
|
||||
{ code: '31', name: 'Dächer', color: '#7a4a3a', lw: 0.35, visible: true, locked: false },
|
||||
{ code: '35', name: 'Träger', color: '#a87858', lw: 0.50, visible: true, locked: false },
|
||||
{ code: '50', name: 'Text', color: '#d0d0d0', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '60', name: 'Plangrafik', color: '#c0a040', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '90', name: 'Referenzen', color: '#585860', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '99', name: 'Konstruktion', color: '#404048', lw: 0.13, visible: true, locked: false },
|
||||
]
|
||||
|
||||
export default function App() {
|
||||
@@ -36,28 +32,12 @@ export default function App() {
|
||||
const [appliedE, setAppliedE] = useState(INITIAL_EBENEN)
|
||||
const [eMode, setEMode] = useState('all')
|
||||
const [hatchPatterns, setHatchPatterns] = useState(['Solid', 'Hatch1', 'Hatch2', 'Hatch3', 'Plus', 'Squares', 'Grid', 'Grid60'])
|
||||
// Ebenenkombinationen (geteilter Store mit Ausschnitten)
|
||||
const [combinations, setCombinations] = useState([]) // Liste { name, layers }
|
||||
const [activeCombName, setActiveCombName] = useState(null) // null = "Eigene"
|
||||
// Dialog fuer "alle bearbeiten" (Pencil-Button)
|
||||
const [combDialog, setCombDialog] = useState(null) // { layers, presets } oder null
|
||||
const wantCombDialogRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
onMessage('STATE_SYNC', ({ ebenen: e, hatchPatterns: hp }) => {
|
||||
if (e) { setEbenen(e); setAppliedE(e) }
|
||||
if (Array.isArray(hp) && hp.length > 0) setHatchPatterns(hp)
|
||||
})
|
||||
onMessage('COMBINATION_DATA', ({ layers, presets }) => {
|
||||
setCombinations(presets || [])
|
||||
if (wantCombDialogRef.current) {
|
||||
wantCombDialogRef.current = false
|
||||
setCombDialog({ layers: layers || [], presets: presets || [] })
|
||||
} else if (combDialog) {
|
||||
// Dialog ist offen — Layer-Liste live aktualisieren (z.B. nach Preset-Save)
|
||||
setCombDialog(d => d ? { ...d, layers: layers || d.layers, presets: presets || [] } : d)
|
||||
}
|
||||
})
|
||||
onMessage('FIRST_RUN', ({ defaultEbenen } = {}) => {
|
||||
// Wenn der Dossier-Launcher ein eigenes Schema definiert hat, nutzen wir
|
||||
// das statt der hardcoded INITIAL_EBENEN.
|
||||
@@ -72,8 +52,6 @@ export default function App() {
|
||||
if (activeCode) setActiveEbene(activeCode)
|
||||
})
|
||||
notifyReady()
|
||||
// Initial Liste der Kombinationen holen
|
||||
setTimeout(() => getCombination(), 200)
|
||||
|
||||
// Native Browser-Context-Menu global unterdruecken
|
||||
const blockContext = (ev) => ev.preventDefault()
|
||||
@@ -122,41 +100,6 @@ export default function App() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [structureKey, appliedStructureKey])
|
||||
|
||||
// --- Ebenen-Kombinationen ----------------------------------------------
|
||||
const handlePickCombination = (name) => {
|
||||
if (!name) { setActiveCombName(null); return }
|
||||
const preset = combinations.find(p => p.name === name)
|
||||
if (!preset) return
|
||||
applyCombination({
|
||||
layers: preset.layers || [],
|
||||
dossierEbenen: preset.dossierEbenen,
|
||||
dossierZeichnungsebenen: preset.dossierZeichnungsebenen,
|
||||
})
|
||||
setActiveCombName(name)
|
||||
}
|
||||
const handleSaveCurrentCombination = () => {
|
||||
const suggested = activeCombName || `Kombi ${combinations.length + 1}`
|
||||
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
|
||||
if (!name) return
|
||||
if (combinations.some(p => p.name === name) &&
|
||||
!window.confirm(`"${name}" überschreiben?`)) return
|
||||
saveCurrentAsCombination(name)
|
||||
setActiveCombName(name)
|
||||
}
|
||||
const handleDeleteCombination = (name) => {
|
||||
if (!name) return
|
||||
if (!window.confirm(`Ebenenkombination "${name}" löschen?`)) return
|
||||
deleteCombinationPreset(name)
|
||||
if (activeCombName === name) setActiveCombName(null)
|
||||
}
|
||||
const handleOpenCombDialog = () => {
|
||||
wantCombDialogRef.current = true
|
||||
getCombination()
|
||||
}
|
||||
const handleUserVisibilityChange = () => {
|
||||
if (activeCombName !== null) setActiveCombName(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
@@ -173,44 +116,8 @@ export default function App() {
|
||||
mode={eMode}
|
||||
onModeChange={setEMode}
|
||||
hatchPatterns={hatchPatterns}
|
||||
combinations={combinations}
|
||||
activeCombName={activeCombName}
|
||||
onPickCombination={handlePickCombination}
|
||||
onSaveCurrentCombination={handleSaveCurrentCombination}
|
||||
onDeleteCombination={handleDeleteCombination}
|
||||
onEditCombinations={handleOpenCombDialog}
|
||||
onUserVisibilityChange={handleUserVisibilityChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{combDialog && (
|
||||
<AusschnittLayerDialog
|
||||
snapName="Ebenenkombinationen bearbeiten"
|
||||
layers={combDialog.layers}
|
||||
presets={combDialog.presets}
|
||||
onClose={() => setCombDialog(null)}
|
||||
onSave={(layers) => {
|
||||
applyCombination(layers)
|
||||
setActiveCombName(null)
|
||||
setCombDialog(null)
|
||||
}}
|
||||
onSavePreset={(name, layers) => {
|
||||
saveCombinationPreset(name, layers)
|
||||
setCombDialog(d => d ? {
|
||||
...d,
|
||||
presets: [...d.presets.filter(p => p.name !== name), { name, layers }],
|
||||
} : d)
|
||||
}}
|
||||
onDeletePreset={(name) => {
|
||||
deleteCombinationPreset(name)
|
||||
setCombDialog(d => d ? {
|
||||
...d,
|
||||
presets: d.presets.filter(p => p.name !== name),
|
||||
} : d)
|
||||
if (activeCombName === name) setActiveCombName(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function send(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[AusschnittSettings] →', type, payload); return }
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
|
||||
function Field({ label, hint, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: '6px 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: '10px 0 4px',
|
||||
borderTop: '1px solid var(--border-light)',
|
||||
marginTop: 8,
|
||||
}}>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AusschnittSettingsApp() {
|
||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||
const [snap, setSnap] = useState(initial.snap || {})
|
||||
const [displayModes, setDisplayModes] = useState(initial.displayModes || [])
|
||||
const [overridesPresets, setOverridesPresets] = useState(initial.overridesPresets || [])
|
||||
const [layerKombis, setLayerKombis] = useState(initial.layerKombis || [])
|
||||
|
||||
useEffect(() => {
|
||||
onMessage('AUSSCHNITT_SETTINGS_STATE', (p) => {
|
||||
if (p.snap) setSnap(p.snap)
|
||||
if (Array.isArray(p.displayModes)) setDisplayModes(p.displayModes)
|
||||
if (Array.isArray(p.overridesPresets)) setOverridesPresets(p.overridesPresets)
|
||||
if (Array.isArray(p.layerKombis)) setLayerKombis(p.layerKombis)
|
||||
})
|
||||
notifyReady()
|
||||
const blockContext = (ev) => ev.preventDefault()
|
||||
document.addEventListener('contextmenu', blockContext)
|
||||
return () => document.removeEventListener('contextmenu', blockContext)
|
||||
}, [])
|
||||
|
||||
const set = (patch) => setSnap(s => ({ ...s, ...patch }))
|
||||
|
||||
const saveAndClose = () => {
|
||||
send('SAVE', {
|
||||
settings: {
|
||||
scale: snap.scale || '',
|
||||
displayMode: snap.displayMode || null,
|
||||
displayModeName: snap.displayModeName || null,
|
||||
applyOverrides: !!snap.applyOverrides,
|
||||
overridesEnabled: !!snap.overridesEnabled,
|
||||
overridesPreset: snap.overridesPreset || '',
|
||||
layerCombination: snap.layerCombination || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'var(--bg-dialog)',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
fontFamily: 'var(--font)', color: 'var(--text-primary)',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{/* Body */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 14px' }}>
|
||||
<Field label="MASSSTAB" hint="z.B. 1:50 — leer für unverändert">
|
||||
<input
|
||||
value={snap.scale || ''}
|
||||
onChange={(ev) => set({ scale: ev.target.value })}
|
||||
placeholder="1:50"
|
||||
style={{ flex: 1, fontSize: 11, fontFamily: 'var(--font-mono)', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="BILDSCHIRMMODUS"
|
||||
hint="Display-Mode des Viewports beim Wiederherstellen">
|
||||
<select
|
||||
value={snap.displayMode || ''}
|
||||
onChange={(ev) => {
|
||||
const dm = displayModes.find(d => d.id === ev.target.value)
|
||||
set({
|
||||
displayMode: ev.target.value || null,
|
||||
displayModeName: dm ? dm.name : null,
|
||||
})
|
||||
}}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
<option value="">— unverändert —</option>
|
||||
{displayModes.map(dm => (
|
||||
<option key={dm.id} value={dm.id}>{dm.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
<SectionLabel>Grafische Overrides</SectionLabel>
|
||||
|
||||
<Field label="OVERRIDES BEIM RESTORE">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!snap.applyOverrides}
|
||||
onChange={(ev) => set({ applyOverrides: ev.target.checked })}
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
|
||||
{snap.applyOverrides
|
||||
? 'Overrides werden gesetzt'
|
||||
: 'Aktueller Overrides-Zustand bleibt'}
|
||||
</span>
|
||||
</Field>
|
||||
|
||||
{snap.applyOverrides && (
|
||||
<>
|
||||
<Field label="OVERRIDES STATUS">
|
||||
<select
|
||||
value={snap.overridesEnabled ? 'on' : 'off'}
|
||||
onChange={(ev) => set({ overridesEnabled: ev.target.value === 'on' })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
<option value="on">AN</option>
|
||||
<option value="off">AUS</option>
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
<Field label="OVERRIDES PRESET"
|
||||
hint="Leer = kein Preset (Doc-Rules bleiben)">
|
||||
<select
|
||||
value={snap.overridesPreset || ''}
|
||||
onChange={(ev) => set({ overridesPreset: ev.target.value })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
disabled={!snap.overridesEnabled}
|
||||
>
|
||||
<option value="">— kein Preset —</option>
|
||||
{overridesPresets.map(name => (
|
||||
<option key={name} value={name}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SectionLabel>Ebenenkombination</SectionLabel>
|
||||
|
||||
<Field label="KOMBI"
|
||||
hint='"Eigene" = die per Snap gespeicherte Sichtbarkeit. Ein Preset überschreibt diese beim Wiederherstellen.'>
|
||||
<select
|
||||
value={snap.layerCombination || ''}
|
||||
onChange={(ev) => set({ layerCombination: ev.target.value })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
<option value="">— Eigene Sichtbarkeit —</option>
|
||||
{layerKombis.map(name => (
|
||||
<option key={name} value={name}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</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={() => send('CANCEL', {})}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={saveAndClose}>Übernehmen</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+25
-58
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import ContextMenu from './components/ContextMenu'
|
||||
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
listAusschnitte, saveAusschnitt, updateAusschnitt,
|
||||
@@ -9,8 +8,7 @@ import {
|
||||
renameAusschnitt, deleteAusschnitt,
|
||||
setAusschnittFolder, setAusschnittScale,
|
||||
duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder,
|
||||
getAusschnittLayers, updateAusschnittLayers,
|
||||
saveLayerPreset, deleteLayerPreset,
|
||||
openAusschnittSettings,
|
||||
} from './lib/rhinoBridge'
|
||||
|
||||
function EditableInline({ value, onCommit, autoEdit, style, fontSize }) {
|
||||
@@ -247,22 +245,16 @@ function RootDropZone({ children, onDragOver, onDragLeave, onDrop, dragOver, emp
|
||||
export default function AusschnitteApp() {
|
||||
const [snaps, setSnaps] = useState([])
|
||||
const [extraFolders, setExtraFolders] = useState([])
|
||||
const [presets, setPresets] = useState([])
|
||||
const [newName, setNewName] = useState('')
|
||||
const [ctxMenu, setCtxMenu] = useState(null)
|
||||
const [collapsed, setCollapsed] = useState({})
|
||||
const [draggingId, setDraggingId] = useState(null)
|
||||
const [dragTarget, setDragTarget] = useState(null)
|
||||
const [layerDialog, setLayerDialog] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
onMessage('LIST', ({ snapshots, folders, presets }) => {
|
||||
onMessage('LIST', ({ snapshots, folders }) => {
|
||||
setSnaps(snapshots || [])
|
||||
setExtraFolders(folders || [])
|
||||
setPresets(presets || [])
|
||||
})
|
||||
onMessage('LAYERS_DATA', ({ id, name, layers, presets }) => {
|
||||
setLayerDialog({ id, name, layers: layers || [], presets: presets || [] })
|
||||
})
|
||||
notifyReady()
|
||||
const blockContext = (ev) => ev.preventDefault()
|
||||
@@ -301,7 +293,7 @@ export default function AusschnitteApp() {
|
||||
{ label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) },
|
||||
{ label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) },
|
||||
{ divider: true },
|
||||
{ label: 'Sichtbarkeit bearbeiten…', icon: 'layers', onClick: () => getAusschnittLayers(id) },
|
||||
{ label: 'Ausschnittseinstellungen…', icon: 'tune', onClick: () => openAusschnittSettings(id) },
|
||||
{ divider: true },
|
||||
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) },
|
||||
{ label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) },
|
||||
@@ -361,17 +353,6 @@ export default function AusschnitteApp() {
|
||||
/>
|
||||
)
|
||||
|
||||
const actions = (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<button className="btn-icon-tonal" onClick={handleAddFolder} title="Neuer Ordner">
|
||||
<Icon name="create_new_folder" size={14} />
|
||||
</button>
|
||||
<button className="btn-icon-tonal" onClick={() => listAusschnitte()} title="Aktualisieren">
|
||||
<Icon name="refresh" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const rootItems = groups[''] || []
|
||||
const isEmpty = snaps.length === 0 && allFolders.length === 0
|
||||
|
||||
@@ -382,21 +363,6 @@ export default function AusschnitteApp() {
|
||||
background: 'var(--bg-base)',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{/* Fixed Header — wie Layouts/Overrides Pattern */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 10px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em',
|
||||
color: 'var(--text-primary)' }}>
|
||||
AUSSCHNITTE
|
||||
</span>
|
||||
<span className="chip" style={{ fontSize: 8 }}>{snaps.length}</span>
|
||||
{actions}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
|
||||
{/* Save-Bar als Card */}
|
||||
<div style={{
|
||||
@@ -481,6 +447,28 @@ export default function AusschnitteApp() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sticky Footer: Anzahl + Ordner erstellen + Reload */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '8px 10px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
background: 'var(--bg-panel)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span className="chip" style={{
|
||||
fontSize: 9, minWidth: 22, justifyContent: 'center',
|
||||
}}>{snaps.length}</span>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
|
||||
Ausschnitte
|
||||
</span>
|
||||
<button className="btn-icon-tonal" onClick={handleAddFolder} title="Neuer Ordner">
|
||||
<Icon name="create_new_folder" size={14} />
|
||||
</button>
|
||||
<button className="btn-icon-tonal" onClick={() => listAusschnitte()} title="Aktualisieren">
|
||||
<Icon name="refresh" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{ctxMenu && (
|
||||
<ContextMenu
|
||||
x={ctxMenu.x} y={ctxMenu.y}
|
||||
@@ -489,27 +477,6 @@ export default function AusschnitteApp() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{layerDialog && (
|
||||
<AusschnittLayerDialog
|
||||
snapName={layerDialog.name}
|
||||
layers={layerDialog.layers}
|
||||
presets={layerDialog.presets}
|
||||
onSave={(layers) => {
|
||||
updateAusschnittLayers(layerDialog.id,
|
||||
layers.map(l => ({ id: l.id, visible: l.visible, locked: l.locked })))
|
||||
setLayerDialog(null)
|
||||
}}
|
||||
onClose={() => setLayerDialog(null)}
|
||||
onSavePreset={(name, layers) => {
|
||||
saveLayerPreset(name, layers)
|
||||
setLayerDialog(d => d ? { ...d, presets: [...d.presets.filter(p => p.name !== name), { name, layers }] } : d)
|
||||
}}
|
||||
onDeletePreset={(name) => {
|
||||
deleteLayerPreset(name)
|
||||
setLayerDialog(d => d ? { ...d, presets: d.presets.filter(p => p.name !== name) } : d)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+53
-79
@@ -50,24 +50,22 @@ function NumInput({ value, onCommit, disabled, suffix, width }) {
|
||||
)
|
||||
}
|
||||
|
||||
// 9-Punkt-Referenzpunkt-Selektor im Illustrator-Stil: sichtbarer BBox-Rahmen,
|
||||
// die Punkte sitzen AUF Ecken / Kantenmitten / Zentrum.
|
||||
// 9-Punkt-Referenzpunkt-Selektor: sichtbarer BBox-Rahmen, Kreise auf den
|
||||
// Eckpunkten / Kantenmitten / Zentrum.
|
||||
function RefPointGrid({ ref, onChange }) {
|
||||
const SIZE = 26 // Aussenkanten-Quadrat (px)
|
||||
const DOT = 5 // Punkt-Durchmesser (px)
|
||||
// Position pro Code: 0% (min), 50% (mid), 100% (max)
|
||||
const SIZE = 28 // Aussenkanten-Quadrat (px)
|
||||
const DOT = 6 // Kreis-Durchmesser (px)
|
||||
const pct = (c) => c === 'min' ? '0%' : c === 'max' ? '100%' : '50%'
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
width: SIZE, height: SIZE,
|
||||
border: '1px solid var(--text-muted)',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'transparent',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{REF_CODES.map(yc => REF_CODES.map(xc => {
|
||||
const active = ref.x === xc && ref.y === yc
|
||||
// yc 'max' = top in user mental model (Vectorworks/Illustrator)
|
||||
const topPct = yc === 'max' ? '0%' : yc === 'min' ? '100%' : '50%'
|
||||
return (
|
||||
<button
|
||||
@@ -79,14 +77,20 @@ function RefPointGrid({ ref, onChange }) {
|
||||
left: pct(xc), top: topPct,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: DOT, height: DOT, padding: 0,
|
||||
borderRadius: 0, // eckig wie Illustrator
|
||||
borderRadius: '50%',
|
||||
background: active ? 'var(--accent)' : 'var(--text-muted)',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
transition: 'background 0.12s, transform 0.12s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!active) e.currentTarget.style.background = 'var(--text-primary)'
|
||||
e.currentTarget.style.transform = 'translate(-50%, -50%) scale(1.25)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!active) e.currentTarget.style.background = 'var(--text-muted)'
|
||||
e.currentTarget.style.transform = 'translate(-50%, -50%) scale(1)'
|
||||
}}
|
||||
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--text-primary)' }}
|
||||
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'var(--text-muted)' }}
|
||||
/>
|
||||
)
|
||||
}))}
|
||||
@@ -189,15 +193,13 @@ export default function DimensionenApp() {
|
||||
background: 'var(--bg-base)', color: 'var(--text-primary)',
|
||||
fontFamily: 'var(--font)', fontSize: 11,
|
||||
}}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 6 }}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden' }}>
|
||||
|
||||
{/* Header: Selektions-Info + World/CPlane */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6,
|
||||
padding: '5px 8px',
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 12px',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<Icon name="select_all" size={14} style={{ color: 'var(--text-muted)' }} />
|
||||
<span style={{ flex: 1, fontWeight: 500 }}>{selLabel()}</span>
|
||||
@@ -221,11 +223,8 @@ export default function DimensionenApp() {
|
||||
<div style={{
|
||||
padding: '32px 16px', textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 11,
|
||||
border: '1px dashed var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
||||
<Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.4 }} />
|
||||
<div style={{ marginTop: 8 }}>Keine Selektion.</div>
|
||||
<div style={{ marginTop: 4, fontSize: 10 }}>
|
||||
In Rhino ein oder mehrere Objekte auswählen.
|
||||
@@ -233,38 +232,30 @@ export default function DimensionenApp() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Referenzpunkt — kompakte einzeilige Card */}
|
||||
{/* Referenzpunkt */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 8px', marginBottom: 6,
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '10px 12px',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
||||
Ref
|
||||
</span>
|
||||
<span className="label-xs" style={{ width: 30 }}>Ref</span>
|
||||
<RefPointGrid ref={ref} onChange={onRefChange} />
|
||||
<div style={{ flex: 1 }} />
|
||||
<RefZSelector z={ref.z} onChange={(z) => onRefChange({ ...ref, z })} />
|
||||
</div>
|
||||
|
||||
{/* Position + Abmessungen nebeneinander */}
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
|
||||
<div style={{
|
||||
flex: 1, display: 'flex', flexDirection: 'column', gap: 4,
|
||||
padding: '6px 8px',
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
minWidth: 0,
|
||||
}}>
|
||||
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.06em', textTransform: 'uppercase',
|
||||
display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>Position</span>
|
||||
<span style={{ fontWeight: 400, textTransform: 'none',
|
||||
fontFamily: 'DM Mono, monospace', fontSize: 9 }}>
|
||||
{/* Position + BBox nebeneinander */}
|
||||
<div style={{
|
||||
display: 'flex', gap: 16,
|
||||
padding: '10px 12px',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between',
|
||||
alignItems: 'baseline', marginBottom: 2 }}>
|
||||
<span className="label-xs">Position</span>
|
||||
<span style={{ fontFamily: 'DM Mono, monospace', fontSize: 9,
|
||||
color: 'var(--text-muted)' }}>
|
||||
{state.planeName}
|
||||
</span>
|
||||
</div>
|
||||
@@ -272,18 +263,9 @@ export default function DimensionenApp() {
|
||||
<Field label="Y"><NumInput value={pos.y} onCommit={(v) => setDimPosition('y', v)} /></Field>
|
||||
<Field label="Z"><NumInput value={pos.z} onCommit={(v) => setDimPosition('z', v)} /></Field>
|
||||
</div>
|
||||
<div style={{
|
||||
flex: 1, display: 'flex', flexDirection: 'column', gap: 4,
|
||||
padding: '6px 8px',
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
minWidth: 0,
|
||||
}}>
|
||||
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
||||
BBox
|
||||
</div>
|
||||
<div style={{ width: 1, background: 'var(--border-light)' }} />
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
||||
<span className="label-xs" style={{ marginBottom: 2 }}>BBox</span>
|
||||
<Field label="B"><NumInput value={dims?.width} onCommit={(v) => setDimDimension('width', v)} /></Field>
|
||||
<Field label="T"><NumInput value={dims?.depth} onCommit={(v) => setDimDimension('depth', v)} /></Field>
|
||||
<Field label="H"><NumInput value={dims?.height} onCommit={(v) => setDimDimension('height', v)} /></Field>
|
||||
@@ -294,22 +276,19 @@ export default function DimensionenApp() {
|
||||
{shape && (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 4,
|
||||
padding: '6px 8px', marginBottom: 6,
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--accent)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
padding: '10px 12px',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--accent)',
|
||||
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
||||
<span className="label-xs" style={{ color: 'var(--accent)' }}>
|
||||
{shape.type === 'circle' && 'Kreis'}
|
||||
{shape.type === 'rectangle' && 'Rechteck'}
|
||||
{shape.type === 'line' && 'Linie'}
|
||||
</div>
|
||||
</span>
|
||||
{shape.type === 'circle' && (
|
||||
<Field label="R"><NumInput value={shape.radius} onCommit={(v) => setCircleRadius(v)} /></Field>
|
||||
)}
|
||||
{shape.type === 'rectangle' && (
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Field label="W" style={{ flex: 1 }}>
|
||||
<NumInput value={shape.width}
|
||||
onCommit={(v) => setRectangleDims(v, shape.height)} />
|
||||
@@ -321,7 +300,7 @@ export default function DimensionenApp() {
|
||||
</div>
|
||||
)}
|
||||
{shape.type === 'line' && (
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Field label="L" style={{ flex: 1 }}>
|
||||
<NumInput value={shape.length} onCommit={(v) => setLineLength(v)} />
|
||||
</Field>
|
||||
@@ -333,19 +312,13 @@ export default function DimensionenApp() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rotation — kompakt einzeilig */}
|
||||
{/* Rotation */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '6px 8px', marginBottom: 6,
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
padding: '10px 12px',
|
||||
}}>
|
||||
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
||||
Drehen
|
||||
</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<span className="label-xs" style={{ width: 50 }}>Drehen</span>
|
||||
<div style={{ width: 56 }}>
|
||||
<NumInput value={rotationDelta} onCommit={setRotationDelta} suffix="°" />
|
||||
</div>
|
||||
<button
|
||||
@@ -357,12 +330,13 @@ export default function DimensionenApp() {
|
||||
>
|
||||
<Icon name="rotate_right" size={13} />
|
||||
</button>
|
||||
<div style={{ flex: 1 }} />
|
||||
{[-90, -45, 45, 90].map(a => (
|
||||
<button
|
||||
key={a}
|
||||
className="btn-outlined"
|
||||
onClick={() => setDimRotationZ(a)}
|
||||
style={{ padding: '3px 5px', fontSize: 9, minWidth: 28 }}
|
||||
style={{ padding: '3px 6px', fontSize: 9, minWidth: 28 }}
|
||||
title={`${a}°`}
|
||||
>
|
||||
{a > 0 ? '+' : ''}{a}°
|
||||
|
||||
+46
-10
@@ -1,37 +1,73 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import EbenenSettingsDialog from './components/EbenenSettingsDialog'
|
||||
import { notifyReady } from './lib/rhinoBridge'
|
||||
import { notifyReady, onMessage } 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
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
|
||||
export default function EbenenSettingsApp() {
|
||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||
|
||||
const [ebenen, setEbenen] = useState(initial.ebenen || [])
|
||||
const [hatchPatterns, setHatchPatterns] = useState(initial.hatchPatterns || ['Solid'])
|
||||
const [selectedCode, setSelectedCode] = useState(initial.currentCode || initial.ebene?.code || '')
|
||||
// Aktuell editiertes Draft. originalCode = Code beim Aufmachen des Editors
|
||||
// (kann sich beim Save aendern wenn User CODE-Feld umbenannt hat).
|
||||
const [originalCode, setOriginalCode] = useState(initial.currentCode || initial.ebene?.code || '')
|
||||
const dialogKey = useRef(0) // Force Dialog-Remount beim Wechsel
|
||||
|
||||
useEffect(() => {
|
||||
onMessage('EBENEN_SETTINGS_STATE', ({ ebenen: list, hatchPatterns: hp }) => {
|
||||
if (Array.isArray(list)) setEbenen(list)
|
||||
if (Array.isArray(hp) && hp.length) setHatchPatterns(hp)
|
||||
})
|
||||
notifyReady()
|
||||
const blockContext = (ev) => ev.preventDefault()
|
||||
document.addEventListener('contextmenu', blockContext)
|
||||
return () => document.removeEventListener('contextmenu', blockContext)
|
||||
}, [])
|
||||
|
||||
const ebene = initial.ebene || initial
|
||||
const hatchPatterns = initial.hatchPatterns || ['Solid']
|
||||
const sortedEbenen = [...ebenen].sort((a, b) => {
|
||||
const ca = parseInt(a.code, 10), cb = parseInt(b.code, 10)
|
||||
if (!isNaN(ca) && !isNaN(cb)) return ca - cb
|
||||
return (a.code || '').localeCompare(b.code || '')
|
||||
})
|
||||
|
||||
if (!ebene || typeof ebene !== 'object' || !ebene.code) {
|
||||
return <div style={{ padding: 20, color: 'var(--text-muted)' }}>Keine Daten</div>
|
||||
const currentEbene = ebenen.find(e => e.code === selectedCode)
|
||||
|| ebenen.find(e => e.code === originalCode)
|
||||
|| initial.ebene
|
||||
|| null
|
||||
|
||||
if (!currentEbene) {
|
||||
return <div style={{ padding: 20, color: 'var(--text-muted)', fontSize: 11 }}>Keine Ebene gefunden</div>
|
||||
}
|
||||
|
||||
const switchTo = (newCode, currentDraft) => {
|
||||
if (!newCode || newCode === selectedCode) return
|
||||
// Aktuelles Draft live persistieren bevor wir wechseln — wenn der User
|
||||
// gerade etwas geaendert hatte ohne 'Übernehmen' zu druecken, geht es
|
||||
// sonst verloren.
|
||||
if (currentDraft && originalCode) {
|
||||
bridgeSend('SAVE_KEEP', { ebene: currentDraft, originalCode })
|
||||
}
|
||||
setSelectedCode(newCode)
|
||||
setOriginalCode(newCode)
|
||||
dialogKey.current += 1 // Force-Remount → frisches Draft aus ebenen[newCode]
|
||||
}
|
||||
|
||||
return (
|
||||
<EbenenSettingsDialog
|
||||
key={dialogKey.current}
|
||||
embedded
|
||||
ebene={ebene}
|
||||
ebene={currentEbene}
|
||||
hatchPatterns={hatchPatterns}
|
||||
onSave={(updated) => bridgeSend('SAVE', updated)}
|
||||
onSave={(updated) => bridgeSend('SAVE', { ebene: updated, originalCode })}
|
||||
onClose={() => bridgeSend('CANCEL', {})}
|
||||
pickerEbenen={sortedEbenen}
|
||||
pickerSelected={currentEbene.code}
|
||||
onPickEbene={(newCode, currentDraft) => switchTo(newCode, currentDraft)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -513,7 +513,7 @@ export default function ElementeApp() {
|
||||
padding: '8px 10px', borderBottom: '1px solid var(--border)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em' }}>ELEMENTE</span>
|
||||
<span style={{ flex: 1, fontWeight: 600 }}>Elemente</span>
|
||||
<span className="chip" style={{ fontSize: 8 }}>{elements.length}</span>
|
||||
<button
|
||||
onClick={() => exportRaeume()}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function send(type, payload) {
|
||||
if (!window.RHINO_MODE) { console.log('[LayerCombinations] →', type, payload); return }
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload: payload || {} })
|
||||
}
|
||||
|
||||
export default function LayerCombinationsApp() {
|
||||
const [layers, setLayers] = useState([])
|
||||
const [presets, setPresets] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
onMessage('LAYER_COMBINATIONS_STATE', ({ layers: ls, presets: ps }) => {
|
||||
if (Array.isArray(ls)) setLayers(ls)
|
||||
if (Array.isArray(ps)) setPresets(ps)
|
||||
})
|
||||
notifyReady()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'var(--bg-base)',
|
||||
display: 'flex',
|
||||
fontFamily: 'var(--font)',
|
||||
color: 'var(--text-primary)',
|
||||
}}>
|
||||
<AusschnittLayerDialog
|
||||
embedded
|
||||
snapName="Ebenenkombinationen"
|
||||
layers={layers}
|
||||
presets={presets}
|
||||
onClose={() => send('CANCEL')}
|
||||
onSave={(draft) => send('APPLY_COMBINATION', {
|
||||
layers: draft.map(l => ({ id: l.id, visible: l.visible, locked: l.locked })),
|
||||
})}
|
||||
onSavePreset={(name, layerStates) => send('SAVE_PRESET', { name, layers: layerStates })}
|
||||
onDeletePreset={(name) => send('DELETE_PRESET', { name })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function send(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[LayoutDialog] →', type, payload); return }
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
|
||||
const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter']
|
||||
|
||||
export default function LayoutDialogApp() {
|
||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||
const [mode, setMode] = useState(initial.mode || 'new')
|
||||
const [layout, setLayout] = useState(initial.layout || null)
|
||||
const [name, setName] = useState('')
|
||||
const [format, setFormat] = useState('A3')
|
||||
const [landscape, setLandscape] = useState(true)
|
||||
const [cw, setCw] = useState('420')
|
||||
const [ch, setCh] = useState('297')
|
||||
|
||||
useEffect(() => {
|
||||
onMessage('LAYOUT_DIALOG_STATE', (p) => {
|
||||
if (p.mode) setMode(p.mode)
|
||||
if (p.layout) {
|
||||
setLayout(p.layout)
|
||||
if (p.mode === 'edit') {
|
||||
setFormat('custom')
|
||||
setCw(String(Math.round(p.layout.width || 420)))
|
||||
setCh(String(Math.round(p.layout.height || 297)))
|
||||
}
|
||||
}
|
||||
})
|
||||
notifyReady()
|
||||
const blockContext = (ev) => ev.preventDefault()
|
||||
document.addEventListener('contextmenu', blockContext)
|
||||
return () => document.removeEventListener('contextmenu', blockContext)
|
||||
}, [])
|
||||
|
||||
const editing = mode === 'edit'
|
||||
|
||||
const submit = () => {
|
||||
const payload = { name: name.trim(), format, landscape }
|
||||
if (format === 'custom') {
|
||||
const w = parseFloat(cw), h = parseFloat(ch)
|
||||
if (!(w > 0) || !(h > 0)) { alert('Bitte gültige Größe eingeben.'); return }
|
||||
payload.customWidth = w
|
||||
payload.customHeight = h
|
||||
}
|
||||
send('SAVE', payload)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'var(--bg-dialog)',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
fontFamily: 'var(--font)', color: 'var(--text-primary)',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{/* Body */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '12px 16px',
|
||||
display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
|
||||
{!editing && (
|
||||
<Field label="Name">
|
||||
<input
|
||||
type="text" value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') submit() }}
|
||||
placeholder="z.B. Grundriss EG"
|
||||
autoFocus
|
||||
style={{ width: '100%', fontSize: 12, padding: '6px 8px' }}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label="Papierformat">
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{PAPER_SIZES.map(f => (
|
||||
<button key={f}
|
||||
onClick={() => setFormat(f)}
|
||||
className={format === f ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '5px 12px', fontSize: 11 }}>
|
||||
{f}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setFormat('custom')}
|
||||
className={format === 'custom' ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '5px 12px', fontSize: 11 }}>
|
||||
Eigene
|
||||
</button>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{format === 'custom' ? (
|
||||
<Field label="Größe (mm)">
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<input
|
||||
type="text" value={cw}
|
||||
onChange={(e) => setCw(e.target.value)}
|
||||
placeholder="Breite"
|
||||
style={{ flex: 1, fontFamily: 'DM Mono, monospace',
|
||||
fontSize: 12, textAlign: 'right', padding: '6px 8px' }}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 11 }}>×</span>
|
||||
<input
|
||||
type="text" value={ch}
|
||||
onChange={(e) => setCh(e.target.value)}
|
||||
placeholder="Höhe"
|
||||
style={{ flex: 1, fontFamily: 'DM Mono, monospace',
|
||||
fontSize: 12, textAlign: 'right', padding: '6px 8px' }}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 10, width: 22 }}>mm</span>
|
||||
</div>
|
||||
</Field>
|
||||
) : (
|
||||
<Field label="Ausrichtung">
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<button
|
||||
onClick={() => setLandscape(true)}
|
||||
className={landscape ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ flex: 1, padding: '8px 12px', fontSize: 11,
|
||||
display: 'flex', gap: 6, alignItems: 'center',
|
||||
justifyContent: 'center' }}>
|
||||
<Icon name="crop_landscape" size={16} /> Quer
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLandscape(false)}
|
||||
className={!landscape ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ flex: 1, padding: '8px 12px', fontSize: 11,
|
||||
display: 'flex', gap: 6, alignItems: 'center',
|
||||
justifyContent: 'center' }}>
|
||||
<Icon name="crop_portrait" size={16} /> Hoch
|
||||
</button>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{/* Preview */}
|
||||
<FormatPreview format={format} landscape={landscape}
|
||||
customW={parseFloat(cw)} customH={parseFloat(ch)} />
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '10px 14px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn-text" onClick={() => send('CANCEL', {})}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={submit}
|
||||
disabled={!editing && !name.trim()}
|
||||
title={!editing && !name.trim() ? 'Erst einen Namen eingeben' : ''}>
|
||||
{editing ? 'Anwenden' : 'Erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Field({ label, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<span className="label-xs">{label}</span>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PAPER_DIMS = {
|
||||
A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420],
|
||||
A4: [210, 297], Letter: [216, 279],
|
||||
}
|
||||
|
||||
function FormatPreview({ format, landscape, customW, customH }) {
|
||||
let w, h
|
||||
if (format === 'custom') { w = customW; h = customH }
|
||||
else {
|
||||
const dims = PAPER_DIMS[format]
|
||||
if (!dims) return null
|
||||
w = dims[0]; h = dims[1]
|
||||
if (landscape) { w = dims[1]; h = dims[0] }
|
||||
}
|
||||
if (!(w > 0) || !(h > 0)) return null
|
||||
const MAX = 120
|
||||
const scale = Math.min(MAX / w, MAX / h)
|
||||
const pw = w * scale, ph = h * scale
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, marginTop: 6 }}>
|
||||
<div style={{
|
||||
width: pw, height: ph,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1.5px solid var(--accent)',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
|
||||
transition: 'width 0.2s, height 0.2s',
|
||||
}} />
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||
fontFamily: 'DM Mono, monospace' }}>
|
||||
{Math.round(w)} × {Math.round(h)} mm
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+74
-198
@@ -3,14 +3,13 @@ import Icon from './components/Icon'
|
||||
import ContextMenu from './components/ContextMenu'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
listLayouts, newLayout, deleteLayout, renameLayout, activateLayout,
|
||||
listLayouts, deleteLayout, renameLayout, activateLayout,
|
||||
addDetail, deleteDetail, bindAusschnitt, syncDetail, syncLayout,
|
||||
setPageSize, exportPdf, exportPdfAll, exportPdfMany,
|
||||
exportPdf, exportPdfAll, exportPdfMany,
|
||||
addLayoutFolder, removeLayoutFolder, setLayoutFolder,
|
||||
openLayoutDialog,
|
||||
} from './lib/rhinoBridge'
|
||||
|
||||
const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter']
|
||||
|
||||
const PAPER_FORMATS_MM = {
|
||||
A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420],
|
||||
A4: [210, 297], Letter: [216, 279],
|
||||
@@ -112,7 +111,6 @@ function EditableName({ value, onCommit, style, title, forceEdit, onEditDone })
|
||||
export default function LayoutsApp() {
|
||||
const [state, setState] = useState({ layouts: [], snapshots: [], details: {}, folders: [] })
|
||||
const [selectedId, setSelectedId] = useState(null)
|
||||
const [dialog, setDialog] = useState(null) // { mode: 'new' | 'edit', layout? }
|
||||
const [checked, setChecked] = useState(new Set()) // Multi-Select IDs
|
||||
const [collapsedFolders, setCollapsedFolders] = useState(new Set())
|
||||
const [draggingId, setDraggingId] = useState(null)
|
||||
@@ -187,8 +185,10 @@ export default function LayoutsApp() {
|
||||
{ divider: true },
|
||||
{ label: 'Als PDF exportieren', icon: 'picture_as_pdf',
|
||||
onClick: () => exportPdf(l.id, 300) },
|
||||
{ label: 'Papierformat aendern', icon: 'aspect_ratio',
|
||||
onClick: () => setDialog({ mode: 'edit', layout: l }) },
|
||||
{ label: 'Papierformat ändern', icon: 'aspect_ratio',
|
||||
onClick: () => openLayoutDialog('edit', {
|
||||
id: l.id, name: l.name, width: l.widthMm, height: l.heightMm,
|
||||
}) },
|
||||
{ divider: true },
|
||||
...(folders.length > 0 ? [
|
||||
...folders.map(f => ({
|
||||
@@ -276,57 +276,6 @@ export default function LayoutsApp() {
|
||||
background: 'var(--bg-base)', color: 'var(--text-primary)',
|
||||
fontFamily: 'var(--font)', fontSize: 11,
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 10px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em' }}>LAYOUTS</span>
|
||||
<button
|
||||
onClick={handleExportSelection}
|
||||
className="btn-icon-tonal"
|
||||
disabled={checked.size === 0}
|
||||
title={checked.size > 0
|
||||
? `Auswahl (${checked.size}) als ein PDF exportieren`
|
||||
: 'Erst Layouts ankreuzen'}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
<Icon name="picture_as_pdf" size={14} />
|
||||
{checked.size > 0 && <span style={{ fontSize: 10 }}>({checked.size})</span>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => exportPdfAll(300)}
|
||||
className="btn-icon-tonal"
|
||||
disabled={layouts.length === 0}
|
||||
title="Alle Layouts als ein PDF exportieren"
|
||||
>
|
||||
<Icon name="picture_as_pdf" size={14} />
|
||||
<span style={{ fontSize: 9, marginLeft: 2 }}>·∗</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNewFolder}
|
||||
className="btn-icon-tonal"
|
||||
title="Neuer Ordner"
|
||||
>
|
||||
<Icon name="create_new_folder" size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDialog({ mode: 'new' })}
|
||||
className="btn-add"
|
||||
title="Neues Layout erstellen"
|
||||
>
|
||||
<Icon name="add" size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => listLayouts()}
|
||||
className="btn-icon-tonal"
|
||||
title="Aktualisieren"
|
||||
>
|
||||
<Icon name="refresh" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: 8 }}>
|
||||
{/* Layout-Liste */}
|
||||
{layouts.length === 0 && folders.length === 0 ? (
|
||||
@@ -340,7 +289,7 @@ export default function LayoutsApp() {
|
||||
<Icon name="dashboard" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
||||
<div style={{ marginTop: 8 }}>Noch keine Layouts.</div>
|
||||
<div style={{ marginTop: 4, fontSize: 10 }}>
|
||||
Oben <Icon name="add" size={11} /> klicken um ein neues Layout anzulegen.
|
||||
Unten <Icon name="add" size={11} /> klicken um ein neues Layout anzulegen.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -610,6 +559,72 @@ export default function LayoutsApp() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sticky Footer: Anzahl + Aktionen */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '8px 10px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
background: 'var(--bg-panel)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span className="chip" style={{
|
||||
fontSize: 9, minWidth: 22, justifyContent: 'center',
|
||||
}}>{layouts.length}</span>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
|
||||
Layouts
|
||||
</span>
|
||||
{/* PDF-Aktionen: feste Breite damit das Auswahl-Counter den Footer
|
||||
nicht horizontal verschiebt. */}
|
||||
<button
|
||||
onClick={handleExportSelection}
|
||||
className="btn-icon-tonal"
|
||||
disabled={checked.size === 0}
|
||||
title={checked.size > 0
|
||||
? `Auswahl (${checked.size}) als ein PDF exportieren`
|
||||
: 'Erst Layouts ankreuzen'}
|
||||
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
|
||||
>
|
||||
<Icon name="picture_as_pdf" size={14} />
|
||||
{checked.size > 0 && (
|
||||
<span style={{ fontSize: 9, fontFamily: 'DM Mono, monospace' }}>
|
||||
{checked.size}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => exportPdfAll(300)}
|
||||
className="btn-icon-tonal"
|
||||
disabled={layouts.length === 0}
|
||||
title="Alle Layouts als ein PDF exportieren"
|
||||
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
|
||||
>
|
||||
<Icon name="picture_as_pdf" size={14} />
|
||||
<span style={{ fontSize: 9 }}>·∗</span>
|
||||
</button>
|
||||
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
|
||||
<button
|
||||
onClick={handleNewFolder}
|
||||
className="btn-icon-tonal"
|
||||
title="Neuer Ordner"
|
||||
>
|
||||
<Icon name="create_new_folder" size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => listLayouts()}
|
||||
className="btn-icon-tonal"
|
||||
title="Aktualisieren"
|
||||
>
|
||||
<Icon name="refresh" size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openLayoutDialog('new', null)}
|
||||
className="btn-add"
|
||||
title="Neues Layout erstellen"
|
||||
>
|
||||
<Icon name="add" size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Kontextmenue */}
|
||||
{ctxMenu && (
|
||||
<ContextMenu
|
||||
@@ -627,145 +642,6 @@ export default function LayoutsApp() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Layout-Dialog: New oder Edit (Papierformat aendern) */}
|
||||
{dialog && (
|
||||
<LayoutDialog
|
||||
mode={dialog.mode}
|
||||
layout={dialog.layout}
|
||||
onCancel={() => setDialog(null)}
|
||||
onSubmit={(p) => {
|
||||
if (dialog.mode === 'new') {
|
||||
newLayout(p.name, p.format, p.landscape, p.customWidth, p.customHeight)
|
||||
} else {
|
||||
setPageSize(dialog.layout.id, p.format, p.landscape, p.customWidth, p.customHeight)
|
||||
}
|
||||
setDialog(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LayoutDialog({ mode, layout, onCancel, onSubmit }) {
|
||||
const editing = mode === 'edit'
|
||||
const [name, setName] = useState('')
|
||||
const [format, setFormat] = useState('A3')
|
||||
const [landscape, setLandscape] = useState(true)
|
||||
const [cw, setCw] = useState('420') // mm
|
||||
const [ch, setCh] = useState('297') // mm
|
||||
|
||||
// Wenn editieren: aktuelle Layout-Groesse pre-fillen (custom-Mode default)
|
||||
useEffect(() => {
|
||||
if (editing && layout) {
|
||||
// BBox in Doc-Einheiten — wir kennen die Einheit nicht direkt im
|
||||
// Frontend. Fuer den Edit-Modus zeigen wir die Groesse als Zahlen an
|
||||
// und schicken sie als "custom" mit der mm-Annahme. Wenn das Doc nicht
|
||||
// auf mm steht, ergibt sich eine kleine Konvertier-Unschaerfe — das
|
||||
// Backend rechnet mm-Werte konsistent in Doc-Units um.
|
||||
setFormat('custom')
|
||||
setCw(String(Math.round(layout.width)))
|
||||
setCh(String(Math.round(layout.height)))
|
||||
}
|
||||
}, [editing, layout])
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0,
|
||||
background: 'rgba(0,0,0,0.55)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
zIndex: 100,
|
||||
}} onClick={(e) => { if (e.target === e.currentTarget) onCancel() }}>
|
||||
<div style={{
|
||||
width: 340, background: 'var(--bg-base)',
|
||||
border: '1px solid var(--border)', borderRadius: 'var(--r-lg)',
|
||||
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{ padding: '10px 12px', borderBottom: '1px solid var(--border)',
|
||||
fontWeight: 600 }}>
|
||||
{editing ? `Papierformat: ${layout?.name}` : 'Neues Layout'}
|
||||
</div>
|
||||
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{!editing && (
|
||||
<div>
|
||||
<div style={labelXs}>Name</div>
|
||||
<input type="text" value={name} onChange={(e) => setName(e.target.value)}
|
||||
placeholder="z.B. Grundriss EG"
|
||||
autoFocus
|
||||
style={{ width: '100%', marginTop: 4 }} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div style={labelXs}>Papierformat</div>
|
||||
<div style={{ display: 'flex', gap: 4, marginTop: 4, flexWrap: 'wrap' }}>
|
||||
{PAPER_SIZES.map(f => (
|
||||
<button key={f}
|
||||
onClick={() => setFormat(f)}
|
||||
className={format === f ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 10px', fontSize: 11 }}>
|
||||
{f}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setFormat('custom')}
|
||||
className={format === 'custom' ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 10px', fontSize: 11 }}>
|
||||
Eigene
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{format === 'custom' ? (
|
||||
<div>
|
||||
<div style={labelXs}>Eigene Groesse (mm)</div>
|
||||
<div style={{ display: 'flex', gap: 6, marginTop: 4, alignItems: 'center' }}>
|
||||
<input type="text" value={cw} onChange={(e) => setCw(e.target.value)}
|
||||
placeholder="Breite"
|
||||
style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} />
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>×</span>
|
||||
<input type="text" value={ch} onChange={(e) => setCh(e.target.value)}
|
||||
placeholder="Höhe"
|
||||
style={{ flex: 1, fontFamily: 'DM Mono, monospace', fontSize: 11 }} />
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>mm</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div style={labelXs}>Ausrichtung</div>
|
||||
<div style={{ display: 'flex', gap: 4, marginTop: 4 }}>
|
||||
<button
|
||||
onClick={() => setLandscape(true)}
|
||||
className={landscape ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 10px', fontSize: 11, display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<Icon name="crop_landscape" size={12} /> Quer
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLandscape(false)}
|
||||
className={!landscape ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 10px', fontSize: 11, display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<Icon name="crop_portrait" size={12} /> Hoch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: 10, borderTop: '1px solid var(--border)',
|
||||
display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
|
||||
<button onClick={onCancel}>Abbrechen</button>
|
||||
<button className="btn-contained"
|
||||
onClick={() => {
|
||||
const payload = { name: name.trim(), format, landscape }
|
||||
if (format === 'custom') {
|
||||
const w = parseFloat(cw), h = parseFloat(ch)
|
||||
if (!(w > 0) || !(h > 0)) { alert('Bitte gueltige Groesse eingeben.'); return }
|
||||
payload.customWidth = w
|
||||
payload.customHeight = h
|
||||
}
|
||||
onSubmit(payload)
|
||||
}}>
|
||||
{editing ? 'Anwenden' : 'Erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
setMassstabDpi, detectMassstabDpi,
|
||||
setView, setDisplayMode,
|
||||
toggleOverrides, setOverridesPreset, openOverridesPanel,
|
||||
pickLayerCombination, saveLayerCombination,
|
||||
deleteLayerCombination, openLayerCombinationsDialog,
|
||||
openDossierSettings,
|
||||
} from './lib/rhinoBridge'
|
||||
|
||||
@@ -125,6 +127,7 @@ export default function OberleisteApp() {
|
||||
overridesEnabled: false, overridesCount: 0,
|
||||
cmdPrompt: '', cmdOptions: [],
|
||||
overridesActivePreset: null, overridesPresets: [],
|
||||
layerCombinations: [], layerCombinationActive: null,
|
||||
})
|
||||
const [appliedScale, setAppliedScale] = useState(null)
|
||||
const appliedScaleRef = useRef(null)
|
||||
@@ -380,6 +383,60 @@ export default function OberleisteApp() {
|
||||
title="Overrides-Regel-Editor öffnen"
|
||||
/>
|
||||
|
||||
<div style={sep} />
|
||||
|
||||
{/* ====== GRUPPE: EBENENKOMBINATION ====== */}
|
||||
<span style={groupLabel}>Kombi</span>
|
||||
<select
|
||||
value={state.layerCombinationActive || '__none__'}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
if (v === '__configure__') { openLayerCombinationsDialog(); return }
|
||||
if (v === '__delete__') {
|
||||
if (state.layerCombinationActive &&
|
||||
window.confirm(`Kombination "${state.layerCombinationActive}" löschen?`))
|
||||
deleteLayerCombination(state.layerCombinationActive)
|
||||
return
|
||||
}
|
||||
pickLayerCombination(v === '__none__' ? null : v)
|
||||
}}
|
||||
style={{ ...pillSelect, width: 140 }}
|
||||
title={state.layerCombinationActive
|
||||
? `Aktive Kombi: ${state.layerCombinationActive}`
|
||||
: 'Keine Kombination — manuelle Sichtbarkeit'}
|
||||
>
|
||||
<option value="__none__">— Eigene —</option>
|
||||
{(state.layerCombinations || []).map(name => (
|
||||
<option key={name} value={name}>{name}</option>
|
||||
))}
|
||||
{state.layerCombinationActive && (
|
||||
<>
|
||||
<option disabled>──────────</option>
|
||||
<option value="__delete__">🗑 Aktuelle löschen</option>
|
||||
</>
|
||||
)}
|
||||
<option disabled>──────────</option>
|
||||
<option value="__configure__">Bearbeiten…</option>
|
||||
</select>
|
||||
<ToolButton
|
||||
onClick={() => {
|
||||
const suggested = state.layerCombinationActive
|
||||
|| `Kombi ${(state.layerCombinations || []).length + 1}`
|
||||
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
|
||||
if (!name) return
|
||||
if ((state.layerCombinations || []).includes(name) &&
|
||||
!window.confirm(`"${name}" überschreiben?`)) return
|
||||
saveLayerCombination(name)
|
||||
}}
|
||||
icon="add"
|
||||
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
|
||||
/>
|
||||
<ToolButton
|
||||
onClick={openLayerCombinationsDialog}
|
||||
icon="edit"
|
||||
title="Ebenenkombinationen bearbeiten"
|
||||
/>
|
||||
|
||||
{/* Spacer am rechten Rand */}
|
||||
<div style={{ flex: 1 }} />
|
||||
</div>
|
||||
|
||||
@@ -56,10 +56,10 @@ export default function ZeichnungsebenenApp() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Sichtbarkeit live anwenden bei Mode-/Visibility-Aenderungen
|
||||
// Sichtbarkeit live anwenden bei Mode-/Visibility-/Lock-Aenderungen
|
||||
const visibilityKey = useMemo(() => (
|
||||
activeId + '|' + zMode + '|' +
|
||||
zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}`).join(',')
|
||||
zeichnungsebenen.map(z => `${z.id}:${z.visible !== false ? 1 : 0}:${z.locked ? 1 : 0}`).join(',')
|
||||
), [activeId, zMode, zeichnungsebenen])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function AusschnittLayerDialog({
|
||||
snapName, layers, presets,
|
||||
onSave, onClose,
|
||||
onSavePreset, onDeletePreset,
|
||||
embedded = false,
|
||||
}) {
|
||||
// Welche Kombination wird gerade angezeigt? null = aktueller Doc-State
|
||||
const [selectedPreset, setSelectedPreset] = useState(null)
|
||||
@@ -104,41 +105,47 @@ export default function AusschnittLayerDialog({
|
||||
onSave(draft)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, zIndex: 150,
|
||||
background: 'var(--bg-overlay)',
|
||||
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
|
||||
paddingTop: 30,
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'var(--bg-dialog)',
|
||||
const wrapperStyle = embedded
|
||||
? { position: 'absolute', inset: 0, display: 'flex' }
|
||||
: { position: 'absolute', inset: 0, zIndex: 150,
|
||||
background: 'var(--bg-overlay)',
|
||||
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
|
||||
paddingTop: 30 }
|
||||
const cardStyle = embedded
|
||||
? { flex: 1, display: 'flex', flexDirection: 'column',
|
||||
background: 'var(--bg-dialog)', overflow: 'hidden' }
|
||||
: { background: 'var(--bg-dialog)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
boxShadow: 'var(--shadow-3)',
|
||||
width: 'calc(100% - 24px)', maxWidth: 480,
|
||||
maxHeight: 'calc(100vh - 60px)',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<Icon name="layers" size={16} style={{ color: 'var(--text-secondary)' }} />
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}>
|
||||
{snapName}
|
||||
</span>
|
||||
{dirty && (
|
||||
<span style={{ fontSize: 10, color: 'var(--warn)',
|
||||
padding: '2px 6px', borderRadius: 'var(--r)',
|
||||
background: 'var(--warn-dim)' }}
|
||||
title="Ungespeicherte Änderungen">●</span>
|
||||
)}
|
||||
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
|
||||
</div>
|
||||
overflow: 'hidden' }
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
<div style={cardStyle}>
|
||||
{/* Header — im embedded-Modus weggelassen (Satellite-Fenster hat schon
|
||||
seine native Title-Bar mit Close-Button) */}
|
||||
{!embedded && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<Icon name="layers" size={16} style={{ color: 'var(--text-secondary)' }} />
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}>
|
||||
{snapName}
|
||||
</span>
|
||||
{dirty && (
|
||||
<span style={{ fontSize: 10, color: 'var(--warn)',
|
||||
padding: '2px 6px', borderRadius: 'var(--r)',
|
||||
background: 'var(--warn-dim)' }}
|
||||
title="Ungespeicherte Änderungen">●</span>
|
||||
)}
|
||||
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preset-Auswahl */}
|
||||
<div style={{
|
||||
@@ -235,7 +242,8 @@ export default function AusschnittLayerDialog({
|
||||
</div>
|
||||
|
||||
{/* Layer-Liste */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', minHeight: 200, maxHeight: '50vh' }}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', minHeight: 200,
|
||||
maxHeight: embedded ? 'none' : '50vh' }}>
|
||||
{filtered.length === 0 ? (
|
||||
<div style={{ padding: '30px 14px', textAlign: 'center', color: 'var(--text-muted)', fontSize: 11 }}>
|
||||
Keine Ebenen gefunden.
|
||||
|
||||
@@ -255,9 +255,6 @@ function SortHeader({ label, sortKey, sortBy, sortDir, onSort, style }) {
|
||||
|
||||
export default function EbenenManager({
|
||||
ebenen, activeCode, onActiveChange, onChange, mode, onModeChange, hatchPatterns,
|
||||
combinations = [], activeCombName = null,
|
||||
onPickCombination, onSaveCurrentCombination, onDeleteCombination,
|
||||
onEditCombinations, onUserVisibilityChange,
|
||||
}) {
|
||||
const [sortBy, setSortBy] = useState('code')
|
||||
const [sortDir, setSortDir] = useState('asc')
|
||||
@@ -306,12 +303,10 @@ export default function EbenenManager({
|
||||
const handleToggleVisible = (code) => {
|
||||
const cur = ebenen.find(e => e.code === code)
|
||||
if (cur) updateByCode(code, { visible: !(cur.visible !== false) })
|
||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
||||
}
|
||||
const handleToggleLock = (code) => {
|
||||
const cur = ebenen.find(e => e.code === code)
|
||||
if (cur) updateByCode(code, { locked: !cur.locked })
|
||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
||||
}
|
||||
const handleColorChange = (code, color) => {
|
||||
updateByCode(code, { color })
|
||||
@@ -426,58 +421,6 @@ export default function EbenenManager({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Ebenenkombinationen — Label + Dropdown + Save-As-Plus */}
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 4,
|
||||
padding: '6px 14px',
|
||||
background: 'var(--bg-section)',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<span className="label-xs">Ebenenkombination</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<select
|
||||
value={activeCombName || '__custom__'}
|
||||
onChange={(ev) => {
|
||||
const v = ev.target.value
|
||||
if (v === '__custom__') return
|
||||
if (v === '__delete__') {
|
||||
if (activeCombName && onDeleteCombination) onDeleteCombination(activeCombName)
|
||||
return
|
||||
}
|
||||
if (onPickCombination) onPickCombination(v)
|
||||
}}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
title={activeCombName ? `Aktiv: ${activeCombName}` : 'Eigene Sichtbarkeit (keine Kombination)'}
|
||||
>
|
||||
<option value="__custom__">{activeCombName ? activeCombName : 'Eigene'}</option>
|
||||
{combinations.length > 0 && <option disabled>──────────</option>}
|
||||
{combinations.map(p => (
|
||||
<option key={p.name} value={p.name}>{p.name}</option>
|
||||
))}
|
||||
{activeCombName && combinations.some(p => p.name === activeCombName) && (
|
||||
<>
|
||||
<option disabled>──────────</option>
|
||||
<option value="__delete__">🗑 Aktuelle löschen</option>
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
<button
|
||||
className="btn-icon-sm"
|
||||
onClick={() => onSaveCurrentCombination && onSaveCurrentCombination()}
|
||||
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
|
||||
>
|
||||
<Icon name="add" size={14} />
|
||||
</button>
|
||||
<button
|
||||
className="btn-icon-sm"
|
||||
onClick={() => onEditCombinations && onEditCombinations()}
|
||||
title="Alle Kombinationen bearbeiten (Dialog)"
|
||||
>
|
||||
<Icon name="edit" size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 4,
|
||||
padding: '6px 14px',
|
||||
@@ -512,7 +455,6 @@ export default function EbenenManager({
|
||||
const anyVisible = ebenen.some(e => e.visible !== false)
|
||||
// Wenn irgendeine sichtbar -> alle aus. Wenn keine sichtbar -> alle an.
|
||||
onChange(ebenen.map(e => ({ ...e, visible: !anyVisible })))
|
||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
||||
}}
|
||||
title={ebenen.every(e => e.visible !== false)
|
||||
? 'Alle Ebenen ausblenden'
|
||||
@@ -534,7 +476,6 @@ export default function EbenenManager({
|
||||
onClick={() => {
|
||||
const anyLocked = ebenen.some(e => e.locked === true)
|
||||
onChange(ebenen.map(e => ({ ...e, locked: !anyLocked })))
|
||||
if (onUserVisibilityChange) onUserVisibilityChange()
|
||||
}}
|
||||
title={ebenen.every(e => e.locked === true)
|
||||
? 'Alle Ebenen entsperren'
|
||||
|
||||
@@ -27,7 +27,10 @@ function SectionLabel({ children }) {
|
||||
)
|
||||
}
|
||||
|
||||
export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false }) {
|
||||
export default function EbenenSettingsDialog({
|
||||
ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false,
|
||||
pickerEbenen = null, pickerSelected = null, onPickEbene = null,
|
||||
}) {
|
||||
const [draft, setDraft] = useState({
|
||||
...ebene,
|
||||
fill: {
|
||||
@@ -107,21 +110,49 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
|
||||
return (
|
||||
<div 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',
|
||||
{/* 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)',
|
||||
}}>
|
||||
{ebene.code} — {ebene.name}
|
||||
</span>
|
||||
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px', lineHeight: 1 }}>×</button>
|
||||
</div>
|
||||
<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' }}>
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import ContextMenu from './ContextMenu'
|
||||
import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge'
|
||||
|
||||
function GeschossBadge({ name }) {
|
||||
return <span className="chip chip-info">{name}</span>
|
||||
}
|
||||
|
||||
function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSettings }) {
|
||||
// Eye-State auch fuer die aktive Zeichnungsebene anzeigen (User-Intention)
|
||||
function ZeichnungsebeneRow({
|
||||
z, active, mode, onClick, onContextMenu,
|
||||
onToggleVisible, onToggleLock, onDelete,
|
||||
}) {
|
||||
const eyeShown = mode !== 'active'
|
||||
const isGeschoss = !!z.isGeschoss
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '4px 12px',
|
||||
margin: active ? '1px 6px' : '0',
|
||||
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
|
||||
// Pill-Form fuer die aktive Zeichnungsebene
|
||||
background: active ? 'var(--active-dim)'
|
||||
: (z.visible !== false) ? 'var(--bg-item)'
|
||||
: 'var(--bg-panel)',
|
||||
borderRadius: active ? 999 : 0,
|
||||
borderLeft: active ? 'none' : '3px solid transparent',
|
||||
borderBottom: active ? 'none' : '1px solid var(--border-light)',
|
||||
@@ -31,7 +37,7 @@ function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSetti
|
||||
fontWeight: active ? 700 : 500,
|
||||
fontSize: 12,
|
||||
color: active ? 'var(--active-light)' : 'var(--text-label)',
|
||||
flex: 1,
|
||||
flex: 1, minWidth: 0,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}>{z.name}</span>
|
||||
|
||||
@@ -65,9 +71,16 @@ function ZeichnungsebeneRow({ z, active, mode, onClick, onToggleVisible, onSetti
|
||||
|
||||
<button
|
||||
className="btn-icon-xs"
|
||||
onClick={(ev) => { ev.stopPropagation(); onSettings() }}
|
||||
title="Einstellungen"
|
||||
><Icon name="settings" size={12} /></button>
|
||||
onClick={(ev) => { ev.stopPropagation(); onToggleLock() }}
|
||||
title={z.locked ? 'Entsperren' : 'Sperren'}
|
||||
style={{ color: z.locked ? 'var(--warn)' : undefined }}
|
||||
><Icon name={z.locked ? 'lock' : 'lock_open'} size={12} /></button>
|
||||
|
||||
<button
|
||||
className="btn-icon-xs"
|
||||
onClick={(ev) => { ev.stopPropagation(); onDelete() }}
|
||||
title="Löschen"
|
||||
><Icon name="close" size={12} /></button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -83,8 +96,7 @@ export default function GeschossManager({
|
||||
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
|
||||
mode, onModeChange,
|
||||
}) {
|
||||
// dialogOpen-State entfaellt — Bearbeiten-Dialog laeuft jetzt als
|
||||
// Satelliten-Fenster via openGeschossDialog().
|
||||
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
||||
|
||||
const sorted = [...zeichnungsebenen].reverse()
|
||||
const gesamthoehe = zeichnungsebenen
|
||||
@@ -93,9 +105,8 @@ export default function GeschossManager({
|
||||
|
||||
const addQuick = () => {
|
||||
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
|
||||
// Plangrafik etc.). User kann via Row-Settings-Cog auf Geschoss
|
||||
// umschalten, oder via Bearbeiten-Dialog (Pencil) ein Geschoss
|
||||
// direkt erstellen.
|
||||
// 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 newZ = {
|
||||
id: `z_${Date.now()}`,
|
||||
@@ -103,7 +114,6 @@ export default function GeschossManager({
|
||||
isGeschoss: false,
|
||||
visible: true,
|
||||
}
|
||||
console.log('[ZEICHNUNGSEBENEN-UI] addQuick →', { newZ, countBefore: zeichnungsebenen.length })
|
||||
onChange([...zeichnungsebenen, newZ])
|
||||
}
|
||||
|
||||
@@ -111,6 +121,56 @@ export default function GeschossManager({
|
||||
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z))
|
||||
}
|
||||
|
||||
const toggleLock = (id) => {
|
||||
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, locked: !z.locked } : z))
|
||||
}
|
||||
|
||||
const duplicate = (id) => {
|
||||
const src = zeichnungsebenen.find(z => z.id === id)
|
||||
if (!src) return
|
||||
const clone = {
|
||||
...src,
|
||||
id: `z_${Date.now()}`,
|
||||
name: `${src.name} Kopie`,
|
||||
}
|
||||
// Direkt nach dem Original einfuegen
|
||||
const idx = zeichnungsebenen.findIndex(z => z.id === id)
|
||||
const next = [...zeichnungsebenen]
|
||||
next.splice(idx + 1, 0, clone)
|
||||
onChange(next)
|
||||
}
|
||||
|
||||
const remove = (id) => {
|
||||
if (zeichnungsebenen.length <= 1) return
|
||||
const target = zeichnungsebenen.find(z => z.id === id)
|
||||
if (!target) return
|
||||
if (!window.confirm(`"${target.name}" wirklich löschen?`)) return
|
||||
onChange(zeichnungsebenen.filter(z => z.id !== id))
|
||||
if (activeId === id) {
|
||||
const next = zeichnungsebenen.find(z => z.id !== id)
|
||||
if (next) onActiveChange(next.id)
|
||||
}
|
||||
}
|
||||
|
||||
const openContextMenu = (ev, id) => {
|
||||
ev.preventDefault(); ev.stopPropagation()
|
||||
setCtxMenu({ x: ev.clientX, y: ev.clientY, id })
|
||||
}
|
||||
|
||||
const ctxItems = (id) => {
|
||||
const z = zeichnungsebenen.find(x => x.id === id)
|
||||
if (!z) return []
|
||||
return [
|
||||
{ label: 'Einstellungen…', icon: 'settings', onClick: () => openGeschossSettings(z) },
|
||||
{ divider: true },
|
||||
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicate(id) },
|
||||
{ divider: true },
|
||||
{ label: 'Löschen', icon: 'delete', danger: true,
|
||||
disabled: zeichnungsebenen.length <= 1,
|
||||
onClick: () => remove(id) },
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{
|
||||
@@ -159,12 +219,21 @@ export default function GeschossManager({
|
||||
active={z.id === activeId}
|
||||
mode={mode}
|
||||
onClick={() => onActiveChange(z.id)}
|
||||
onContextMenu={(ev) => openContextMenu(ev, z.id)}
|
||||
onToggleVisible={() => toggleVisible(z.id)}
|
||||
onSettings={() => openGeschossSettings(z)}
|
||||
onToggleLock={() => toggleLock(z.id)}
|
||||
onDelete={() => remove(z.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{ctxMenu && (
|
||||
<ContextMenu
|
||||
x={ctxMenu.x} y={ctxMenu.y}
|
||||
items={ctxItems(ctxMenu.id)}
|
||||
onClose={() => setCtxMenu(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+10
-1
@@ -112,6 +112,7 @@ export function getAusschnittLayers(id) { send('GET_LAYERS', { i
|
||||
export function updateAusschnittLayers(id, layers) { send('UPDATE_LAYERS', { id, layers }) }
|
||||
export function saveLayerPreset(name, layers) { send('SAVE_PRESET', { name, layers }) }
|
||||
export function deleteLayerPreset(name) { send('DELETE_PRESET', { name }) }
|
||||
export function openAusschnittSettings(id) { send('OPEN_SETTINGS', { id }) }
|
||||
|
||||
// --- Gestaltung-Panel ---
|
||||
export function requestSelection() {
|
||||
@@ -168,6 +169,11 @@ export function toggleOverrides(on) { send('TOGGLE_OVERRIDES', { enabled: !
|
||||
export function setOverridesPreset(name) { send('SET_OVERRIDES_PRESET', { name: name || null }) }
|
||||
export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) }
|
||||
export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) }
|
||||
// Ebenenkombinationen (gehosted in Oberleiste, gleicher Store wie EBENEN)
|
||||
export function pickLayerCombination(name) { send('PICK_LAYER_COMBINATION', { name: name || null }) }
|
||||
export function saveLayerCombination(name) { send('SAVE_LAYER_COMBINATION', { name }) }
|
||||
export function deleteLayerCombination(name) { send('DELETE_LAYER_COMBINATION', { name }) }
|
||||
export function openLayerCombinationsDialog() { send('OPEN_LAYER_COMBINATIONS_DIALOG', {}) }
|
||||
export function runCommand(cmd) { send('RUN_COMMAND', { cmd }) }
|
||||
export function sendKeys(text, enter) { send('SEND_KEYS', { text, enter: enter !== false }) }
|
||||
export function cancelCommand() { send('CANCEL_COMMAND', {}) }
|
||||
@@ -232,6 +238,7 @@ export function setPageSize(id, format, landscape, customWidth, customHeight) {
|
||||
customWidth, customHeight })
|
||||
}
|
||||
export function exportPdf(id, dpi) { send('EXPORT_PDF', { id, dpi: dpi || 300 }) }
|
||||
export function openLayoutDialog(mode, layout) { send('OPEN_LAYOUT_DIALOG', { mode: mode || 'new', layout: layout || null }) }
|
||||
export function exportPdfAll(dpi) { send('EXPORT_PDF', { dpi: dpi || 300 }) }
|
||||
export function exportPdfMany(ids, dpi) { send('EXPORT_PDF', { ids, dpi: dpi || 300 }) }
|
||||
export function addLayoutFolder(name) { send('ADD_FOLDER', { name }) }
|
||||
@@ -304,7 +311,9 @@ export function applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, z
|
||||
const zList = Array.isArray(a.zeichnungsebenen) ? a.zeichnungsebenen : []
|
||||
const eList = Array.isArray(a.ebenen) ? a.ebenen : []
|
||||
const slimZ = zList.map(z => ({
|
||||
id: z.id, name: z.name, visible: z.visible !== false,
|
||||
id: z.id, name: z.name,
|
||||
visible: z.visible !== false,
|
||||
locked: z.locked === true,
|
||||
}))
|
||||
const slimE = eList.map(e => ({
|
||||
code: e.code, visible: e.visible !== false, locked: e.locked === true,
|
||||
|
||||
@@ -6,6 +6,9 @@ import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx'
|
||||
import GeschossSettingsApp from './GeschossSettingsApp.jsx'
|
||||
import EbenenSettingsApp from './EbenenSettingsApp.jsx'
|
||||
import GeschossDialogApp from './GeschossDialogApp.jsx'
|
||||
import LayerCombinationsApp from './LayerCombinationsApp.jsx'
|
||||
import AusschnittSettingsApp from './AusschnittSettingsApp.jsx'
|
||||
import LayoutDialogApp from './LayoutDialogApp.jsx'
|
||||
import GestaltungApp from './GestaltungApp.jsx'
|
||||
import AusschnitteApp from './AusschnitteApp.jsx'
|
||||
import MassstabApp from './MassstabApp.jsx'
|
||||
@@ -30,6 +33,9 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
|
||||
: mode === 'geschoss_settings' ? GeschossSettingsApp
|
||||
: mode === 'ebenen_settings' ? EbenenSettingsApp
|
||||
: mode === 'geschoss_dialog' ? GeschossDialogApp
|
||||
: mode === 'layer_combinations' ? LayerCombinationsApp
|
||||
: mode === 'ausschnitt_settings' ? AusschnittSettingsApp
|
||||
: mode === 'layout_dialog' ? LayoutDialogApp
|
||||
: App
|
||||
|
||||
window.onerror = function (msg, src, line, col, err) {
|
||||
|
||||
Reference in New Issue
Block a user