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:
2026-05-19 03:58:28 +02:00
parent e3918cb155
commit 95031ee2c0
29 changed files with 1708 additions and 713 deletions
+19 -112
View File
@@ -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>
)
}
+187
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()}
+44
View File
@@ -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>
)
}
+207
View File
@@ -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
View File
@@ -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>
)
}
+57
View File
@@ -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>
+2 -2
View File
@@ -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(() => {
+38 -30
View File
@@ -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.
-59
View File
@@ -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'
+46 -15
View File
@@ -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' }}>
+85 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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) {