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: 'view_quilt', label: 'Top' }, { value: 'Front', icon: 'north', label: 'Front' }, { value: 'Right', icon: 'east', label: 'Right' }, { value: 'Perspective', icon: 'view_in_ar', 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 (