import { useState, useEffect, useRef } from 'react' import Icon from './components/Icon' import { onMessage, notifyReady, requestMassstab, setMassstab, zoomExtents, zoomSelection, setShowLineweights, setMassstabDpi, detectMassstabDpi, setView, setDisplayMode, toggleOverrides, setOverridesPreset, openOverridesPanel, pickLayerCombination, saveLayerCombination, deleteLayerCombination, openLayerCombinationsDialog, openDossierSettings, } from './lib/rhinoBridge' const PRESETS = [ { value: 1, label: '1:1' }, { value: 5, label: '1:5' }, { value: 10, label: '1:10' }, { value: 20, label: '1:20' }, { value: 25, label: '1:25' }, { value: 50, label: '1:50' }, { value: 100, label: '1:100' }, { value: 200, label: '1:200' }, { value: 500, label: '1:500' }, { value: 1000, label: '1:1000' }, ] const VIEWS = [ { value: 'Top', icon: 'north', label: 'Top' }, { value: 'Front', icon: 'view_in_ar', label: 'Front' }, { value: 'Right', icon: 'east', label: 'Right' }, { value: 'Perspective', icon: 'view_quilt', label: 'Persp' }, ] function fmtScale(s) { if (s == null) return '—' if (s >= 1) return '1:' + (s >= 10 ? s.toFixed(0) : s.toFixed(1)) return (1 / s).toFixed(2) + ':1' } function snapToPreset(s, tol = 0.03) { if (s == null) return null for (const p of PRESETS) { if (Math.abs(s - p.value) / p.value <= tol) return p.value } return null } function parseScale(input) { if (!input) return null const s = String(input).trim() for (const sep of [':', '=', '/']) { if (s.includes(sep)) { const [a, b] = s.split(sep, 2) const pa = parseFloat(a), pb = parseFloat(b) if (pa > 0 && pb > 0) return pb / pa return null } } const n = parseFloat(s) return n > 0 ? n : null } // --------------------------------------------------------------------------- // Material-UI-Style: alle Pills auf einheitliche Hoehe (PILL_H) // nutzt globale Klassen aus index.css fuer Look (border-radius, colors) const PILL_H = 20 // Einheits-Hoehe fuer alle Bar-Elemente const sep = { width: 1, height: 18, background: 'var(--border)', flexShrink: 0, margin: '0 2px', } const groupLabel = { fontSize: 9, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600, alignSelf: 'center', whiteSpace: 'nowrap', padding: '0 4px', } const pillSelect = { height: PILL_H, lineHeight: PILL_H + 'px', padding: '0 20px 0 10px', boxSizing: 'border-box', fontSize: 10, } const pillInput = { height: PILL_H, lineHeight: PILL_H + 'px', padding: '0 8px', boxSizing: 'border-box', borderRadius: 999, fontSize: 10, } const pillChip = { height: PILL_H, lineHeight: PILL_H + 'px', padding: '0 8px', boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', fontSize: 9, } const pillIconBtn = { width: PILL_H, height: PILL_H, borderRadius: '50%', boxSizing: 'border-box', } // Tonal button helper (filled when active, outlined when not) function ToolButton({ active, onClick, icon, label, title, disabled }) { return ( ) } // --------------------------------------------------------------------------- export default function OberleisteApp() { const [state, setState] = useState({ viewName: null, parallel: false, scale: null, pixelWidth: null, pixelHeight: null, unitSystem: '?', dpi: 96, dpiSource: 'default', showLineweights: false, viewMode: null, displayMode: null, displayModes: [], ortho: false, gridSnap: false, osnap: false, overridesEnabled: false, overridesCount: 0, cmdPrompt: '', cmdOptions: [], overridesActivePreset: null, overridesPresets: [], layerCombinations: [], layerCombinationActive: null, }) const [appliedScale, setAppliedScale] = useState(null) const appliedScaleRef = useRef(null) const [draft, setDraft] = useState('') const [customMode, setCustomMode] = useState(false) // Dropdown -> Custom-Input switch const customInputRef = useRef(null) useEffect(() => { onMessage('STATE', (s) => { setState((prev) => ({ ...prev, ...s })) // Dropdown spiegelt EXAKT den Backend-appliedScale fuer den aktuellen // Viewport. Kein Live-Skala-Fallback — das Dropdown ist statisch und // pro Viewport gebunden. Backend gibt null zurueck wenn der aktive // Viewport noch keinen gesetzten Massstab hat → Dropdown zeigt "1:?". const next = (typeof s?.appliedScale === 'number' && s.appliedScale > 0) ? s.appliedScale : null if (next !== appliedScaleRef.current) { setAppliedScale(next) appliedScaleRef.current = next } }) notifyReady() setTimeout(() => requestMassstab(), 50) }, []) const isPerspective = state.parallel === false const scaleVal = state.scale const dropdownValue = appliedScale != null ? String(appliedScale) : '__none__' const applyDropdown = (val) => { if (val === '__none__') return if (val === '__custom__') { setDraft(appliedScale ? `1:${appliedScale}` : '') setCustomMode(true) // Nach dem Render: Focus + Selektion fuer schnelles Eintippen. setTimeout(() => { customInputRef.current?.focus() customInputRef.current?.select() }, 0) return } const r = parseFloat(val) if (r > 0) { setAppliedScale(r); appliedScaleRef.current = r; setMassstab(r) } } const applyDraft = () => { const r = parseScale(draft) if (r != null) { setAppliedScale(r); appliedScaleRef.current = r; setMassstab(r) } setDraft('') setCustomMode(false) } const cancelDraft = () => { setDraft(''); setCustomMode(false) } const apply100 = () => { if (appliedScale && appliedScale > 0) setMassstab(appliedScale) } // Aktuelles View-Match (manche User haben "Top" / "Right" als view name) const matchView = (v) => { if (!state.viewName) return false return state.viewName === v || state.viewName.toLowerCase() === v.toLowerCase() } // (Command-Bar wurde entfernt — Rhinos eigene Command-Line wird benutzt.) return (
{/* === Toolbar (View, Display, Massstab, Snap, Overrides) === */}
DOSSIER {__APP_VERSION__}
{/* ====== GRUPPE: VIEW ====== */} View {VIEWS.map(v => ( setView(v.value)} active={matchView(v.value)} icon={v.icon} label={v.label} title={`Ansicht ${v.label}`} /> ))}
{/* ====== GRUPPE: DISPLAY-MODE ====== */} Display
{/* ====== GRUPPE: MASSSTAB ====== */} Massstab {/* Live-Zoom des Viewports — immer sichtbar bei Parallelprojektion, unabhaengig davon ob ein Massstab im Dropdown gepinnt ist oder nicht. Nur in Perspective ist die Anzeige nicht sinnvoll. */} {isPerspective ? '—' : fmtScale(scaleVal)} {customMode ? ( setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') applyDraft() else if (e.key === 'Escape') cancelDraft() }} onBlur={applyDraft} style={{ ...pillInput, width: 92 }} title="Massstab eingeben (Enter = uebernehmen, Esc = abbrechen)" /> ) : ( )} = 10 ? Math.round(appliedScale) : appliedScale})` : 'Erst einen Massstab wählen'} /> setShowLineweights(!state.showLineweights)} active={state.showLineweights} label={state.showLineweights ? 'Print' : 'Edit'} title={state.showLineweights ? 'Print-View aktiv — klick zum Ausschalten' : 'Strichstärken anzeigen (Print-View)'} icon={state.showLineweights ? 'print' : 'edit'} />
{/* Snap-Toggles (Ortho/Grid/OSnap) sind in Rhinos eigener Footer-Bar schon vorhanden — hier rausgenommen um Doppelung zu vermeiden. */} {/* ====== GRUPPE: OVERRIDES ====== */} Overrides toggleOverrides(!state.overridesEnabled)} active={state.overridesEnabled} icon="auto_fix_high" label={state.overridesEnabled ? 'AN' : 'AUS'} title={state.overridesEnabled ? `Grafische Overrides aktiv — klick zum Ausschalten` : `Grafische Overrides ausgeschaltet`} /> {/* Preset-Dropdown: aktive Kombination waehlen. "—" = keine Kombination (Doc-Rules sind frei editiert oder leer). "Konfigurieren…" oeffnet den grossen Regel-Editor (OVERRIDES-Panel). */}
{/* ====== GRUPPE: EBENENKOMBINATION ====== */} Kombi { 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" /> {/* Spacer am rechten Rand */}
) }