import { useState, useEffect } from 'react' import Icon from './components/Icon' import { onMessage, notifyReady, setKameraViewport, setKameraProjection, setKameraIso, kameraZoomExtents, saveKameraPreset, applyKameraPreset, deleteKameraPreset, setKameraNorthAngle, } from './lib/rhinoBridge' const labelXs = { fontSize: 9, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, } function NumberField({ label, value, onCommit, suffix, step = 0.1 }) { const [draft, setDraft] = useState(value != null ? value.toFixed(3) : '') useEffect(() => { setDraft(value != null ? value.toFixed(3) : '') }, [value]) const commit = () => { const n = parseFloat(draft) if (!isNaN(n)) onCommit(n) else setDraft(value != null ? value.toFixed(3) : '') } return (
{label}
setDraft(ev.target.value)} onBlur={commit} onKeyDown={(ev) => { if (ev.key === 'Enter') commit() if (ev.key === 'Escape') setDraft(value != null ? value.toFixed(3) : '') }} style={{ flex: 1, fontSize: 11, padding: '4px 8px', fontFamily: 'var(--font-mono)' }} /> {suffix && ( {suffix} )}
) } function Vec3Field({ label, value, onCommit }) { const v = value || [0, 0, 0] return (
{label}
{['X', 'Y', 'Z'].map((axis, i) => ( { const next = [v[0], v[1], v[2]] next[i] = n onCommit(next) }} /> ))}
) } export default function KameraApp() { const [vp, setVp] = useState(null) const [presets, setPresets] = useState([]) const [presetName, setPresetName] = useState('') const [northAngle, setNorthAngleState] = useState(0) useEffect(() => { onMessage('STATE', (s) => { setVp(s.viewport || null) setPresets(s.presets || []) if (typeof s.northAngle === 'number') setNorthAngleState(s.northAngle) }) notifyReady() }, []) if (!vp) { return (
Kein aktiver Viewport.
) } const isPar = !!vp.parallel const updateLoc = (loc) => setKameraViewport({ loc }) const updateTgt = (target) => setKameraViewport({ target }) const updateLens = (lensMm) => setKameraViewport({ lensMm }) const updateFW = (frustumW) => setKameraViewport({ frustumW }) const saveCurrent = () => { const n = (presetName || '').trim() if (!n) return saveKameraPreset(n) setPresetName('') } return (
{/* Header: Viewport-Name + Projektion-Toggle */}
Viewport
{vp.name || 'Unnamed'}
{/* Plan-Norden — Rotations-Winkel im Uhrzeigersinn von +Y */}
Plan-Norden (Rotation von +Y, im Uhrzeigersinn)
{ const a = parseFloat(e.target.value) if (!isNaN(a)) { setNorthAngleState(a) setKameraNorthAngle(a) } }} style={{ flex: 1, fontSize: 12, padding: '4px 8px', fontFamily: 'DM Mono, monospace' }} /> °
Norden = +Y bei 0°. Bei rotierten Projekten (z.B. swissBUILDINGS in LV95-Orientierung oder Sonnenberechnungen) hier den Plan-Norden in Grad eintragen. Wirkt auf TOP, ISO und N/O/S/W-Ansichten.
{/* Iso-Quick-Picker */}
Isometrie (Standard, true-iso 35°/45°)
{[ { v: 'NW', label: 'NW' }, { v: 'NE', label: 'NE' }, { v: 'SE', label: 'SE' }, { v: 'SW', label: 'SW' }, ].map(o => ( ))}
{/* Kamera-Position + Target */} {/* Distance read-only */}
Distanz {vp.distance != null ? vp.distance.toFixed(2) + ' m' : '—'}
{/* Linse / Frustum je nach Projektion */} {isPar ? ( ) : ( )} {/* Presets */}
Presets
setPresetName(ev.target.value)} onKeyDown={(ev) => { if (ev.key === 'Enter') saveCurrent() }} style={{ flex: 1, fontSize: 11, padding: '4px 8px' }} />
{presets.length === 0 ? ( Keine Presets gespeichert. ) : (
{presets.map(p => (
{p.name} {p.parallel ? 'Par' : 'Persp'}
))}
)}
) }