Files
DOSSIER/src/ScaleApp.jsx
T
karim 375487c10c i18n DE/EN + DossierSettings panel + English file renames
i18n:
- src/i18n/de.json + en.json: 200+ keys covering all main panels
- src/i18n/index.js: t(key, vars) reads window.DOSSIER_LANG
- panel_base.py: injects window.DOSSIER_LANG from dossier_settings.json
- EbenenManager, GeschossManager, AusschnitteApp, LayoutsApp: all
  context menus and main labels use t()

DossierSettings panel:
- DossierSettingsApp.jsx: language toggle (DE/EN pill) + launcher status
- toolbar.py: OPEN_SETTINGS opens new Rhino-hosted satellite window,
  SAVE_LANG writes lang to dossier_settings.json + reloads all panels

File renames (JSX → English):
- ZeichnungsebenenApp → DrawingLevelsApp
- GeschossManager/Dialog/Settings → Floor*
- AusschnitteApp/Settings → Viewports*
- EbenenManager/Settings → Layer*
- GestaltungApp → StylesApp, OberleisteApp → ToolbarApp
- WerkzeugeApp → ToolsApp, DimensionenApp → DimensionsApp
- MassstabApp → ScaleApp, KameraApp → CameraApp
- MasseSettingsApp → UnitsSettingsApp
- ConfirmDeleteEbene → ConfirmDeleteLayer
- AusschnittLayerDialog → ViewportLayerDialog

Python module renames:
- rhinopanel.py → layers_panel.py
- oberleiste.py → toolbar.py
- gestaltung.py → styles.py
- werkzeuge.py → tools.py
- dimensionen.py → dimensions.py
- startup.py _MODULE_TO_PY updated, all cross-imports fixed
2026-06-06 11:09:33 +02:00

273 lines
9.2 KiB
React

// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect, useRef } from 'react'
import Icon from './components/Icon'
import { BarToggle, BarButton, BarCombo, BAR_H } from './components/BarControls'
import {
onMessage, notifyReady,
requestMassstab, setMassstab,
zoomOneToOne, zoomExtents, zoomSelection,
setMassstabDpi, detectMassstabDpi, setShowLineweights,
} 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'},
]
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)
const pb = parseFloat(b)
if (pa > 0 && pb > 0) return pb / pa
return null
}
}
const n = parseFloat(s)
return n > 0 ? n : null
}
const pillInput = {
height: BAR_H,
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 999,
color: 'var(--text-primary)',
fontSize: 11, fontFamily: 'var(--font)',
padding: '0 10px',
outline: 'none',
boxSizing: 'border-box',
}
// ---------------------------------------------------------------------------
export default function MassstabApp() {
const [state, setState] = useState({
viewName: null, parallel: false, scale: null,
pixelWidth: null, pixelHeight: null, unitSystem: '?',
dpi: 96, dpiSource: 'default',
showLineweights: false,
})
const [appliedScale, setAppliedScale] = useState(null)
const appliedScaleRef = useRef(null)
const [draft, setDraft] = useState('')
const [dpiOpen, setDpiOpen] = useState(false)
const [dpiDraft, setDpiDraft] = useState('96')
useEffect(() => {
onMessage('STATE', (s) => {
setState((prev) => ({ ...prev, ...s }))
// Backend appliedScale (gilt nur fuer aktuellen Viewport) > Live-Snap > roher Live-Wert
let next = null
if (typeof s?.appliedScale === 'number' && s.appliedScale > 0) {
next = s.appliedScale
} else if (s?.parallel && typeof s?.scale === 'number' && s.scale > 0) {
const snap = snapToPreset(s.scale)
next = snap != null ? snap : Math.round(s.scale * 10) / 10
}
if (next != null && next > 0 && 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
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('')
}
}
const commitDpi = () => {
const v = parseFloat(dpiDraft)
if (v >= 30 && v <= 600) setMassstabDpi(v)
setDpiOpen(false)
}
// "100%" = Viewport-Zoom auf den aktuell eingestellten Massstab snappen
// (nicht: Massstab auf 1:1 setzen). Praktisch nach Pan/Zoom, um wieder
// zur Soll-Skala zu kommen.
const apply100 = () => {
if (appliedScale && appliedScale > 0) {
setMassstab(appliedScale)
}
}
return (
<div style={{
width: '100%', height: '100%',
display: 'flex', alignItems: 'center', gap: 6,
padding: '4px 8px',
fontFamily: 'var(--font)', color: 'var(--text-primary)',
background: 'var(--bg-base)',
borderTop: '1px solid var(--border)',
boxSizing: 'border-box',
overflow: 'hidden',
}}>
{/* Live-Zoom Anzeige */}
<div style={{
fontSize: 13, fontWeight: 600,
fontFamily: 'DM Mono, monospace',
minWidth: 56, textAlign: 'right',
color: isPerspective ? 'var(--text-muted)' : 'var(--text-primary)',
}} title={isPerspective ? 'Perspective — kein Massstab'
: `Aktueller Zoom (live)${appliedScale!=null ? ` · gesetzt 1:${appliedScale}` : ''}`}>
{isPerspective ? '—' : fmtScale(scaleVal)}
</div>
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
{/* Skala-Dropdown */}
<BarCombo
disabled={isPerspective}
value={dropdownValue}
onChange={applyDropdown}
width={84}
title="Massstab wählen">
<option value="__none__">1:?</option>
{PRESETS.map(p => (
<option key={p.value} value={String(p.value)}>{p.label}</option>
))}
{appliedScale != null && !PRESETS.some(p => p.value === appliedScale) && (
<option value={String(appliedScale)}>1:{appliedScale}</option>
)}
</BarCombo>
{/* Freitext */}
<input
disabled={isPerspective}
type="text"
placeholder="1:N"
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') applyDraft() }}
onBlur={() => { if (draft) applyDraft() }}
style={{ ...pillInput, width: 64 }}
title="Eigenen Massstab eingeben (Enter)"
/>
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
{/* Aktions-Buttons */}
<BarToggle label="100%"
disabled={isPerspective || !appliedScale}
onClick={apply100}
title={appliedScale
? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})`
: 'Erst einen Massstab wählen'} />
<BarButton icon="fit_screen"
onClick={zoomExtents}
title="Auf gesamten Inhalt zoomen" />
<BarButton icon="center_focus_strong"
onClick={zoomSelection}
title="Auf Selektion zoomen" />
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
{/* Print-View / Strichstaerken-Toggle */}
<BarToggle
icon={state.showLineweights ? 'print' : 'edit'}
label={state.showLineweights ? 'Print' : 'Edit'}
active={state.showLineweights}
onClick={() => setShowLineweights(!state.showLineweights)}
title={state.showLineweights
? 'Strichstärken werden angezeigt (Print-View) — klicken zum Ausschalten'
: 'Strichstärken als Hairlines (Edit-View) — klicken um Print-View zu zeigen'} />
{/* Spacer */}
<div style={{ flex: 1 }} />
{/* View-Name */}
<div style={{
fontSize: 10, color: 'var(--text-muted)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
maxWidth: 140,
}} title={state.viewName || ''}>
{state.viewName || '?'}{isPerspective ? ' · Persp.' : ''}
</div>
{/* DPI-Popover */}
<div style={{ position: 'relative' }}>
<BarToggle
label={`${Math.round(state.dpi || 96)}dpi${state.dpiSource === 'auto' ? ' · auto' : ''}`}
active={state.dpiSource === 'auto'}
onClick={() => { setDpiDraft(String(state.dpi || 96)); setDpiOpen(o => !o) }}
title={`DPI Kalibrierung — aktuell ${Math.round(state.dpi || 96)} dpi (${state.dpiSource || 'default'})`} />
{dpiOpen && (
<div style={{
position: 'absolute', right: 0, bottom: '100%', marginBottom: 4,
background: 'var(--bg-panel)', border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)', padding: 8, display: 'flex',
flexDirection: 'column', gap: 6,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)', zIndex: 10, minWidth: 220,
}}>
<div style={{ fontSize: 10, color: 'var(--text-muted)' }}>
Bildschirm-Auflösung für Massstab-Berechnung
</div>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input
type="number" min={30} max={600}
value={dpiDraft}
onChange={(e) => setDpiDraft(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') commitDpi() }}
autoFocus
style={{ ...pillInput, flex: 1 }}
/>
<BarToggle label="OK" active onClick={commitDpi} />
</div>
<BarToggle icon="auto_fix_high" label="Auto-Detect (EDID)"
onClick={() => { detectMassstabDpi(); setDpiOpen(false) }}
title="DPI automatisch über EDID des Bildschirms ermitteln" />
</div>
)}
</div>
</div>
)
}