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 (
)
}
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'}
setKameraProjection(false)}
className={!isPar ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 12px', fontSize: 10, border: 'none',
borderRadius: 0 }}
>Perspektive
setKameraProjection(true)}
className={isPar ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 12px', fontSize: 10, border: 'none',
borderRadius: 0 }}
>Parallel
{/* Plan-Norden — Rotations-Winkel im Uhrzeigersinn von +Y */}
{/* 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 => (
setKameraIso(o.v)}
className="btn-outlined"
style={{ padding: '6px 0', fontSize: 11 }}
title={`Isometrie aus ${o.label} (Kamera blickt Richtung Szene)`}
>{o.label}
))}
{/* Kamera-Position + Target */}
{/* Distance read-only */}
Distanz
{vp.distance != null ? vp.distance.toFixed(2) + ' m' : '—'}
{/* Linse / Frustum je nach Projektion */}
{isPar ? (
) : (
)}
kameraZoomExtents()}
className="btn-outlined"
style={{ padding: '6px 12px', fontSize: 11 }}
>
Zoom Extents
{/* Presets */}
Presets
setPresetName(ev.target.value)}
onKeyDown={(ev) => { if (ev.key === 'Enter') saveCurrent() }}
style={{ flex: 1, fontSize: 11, padding: '4px 8px' }}
/>
Aktuelle speichern
{presets.length === 0 ? (
Keine Presets gespeichert.
) : (
{presets.map(p => (
applyKameraPreset(p.id)}
className="btn-outlined"
style={{ padding: '2px 8px', fontSize: 10 }}
title="Anwenden"
>
{p.name}
{p.parallel ? 'Par' : 'Persp'}
deleteKameraPreset(p.id)}
className="btn-icon-xs"
title="Loeschen"
>
))}
)}
)
}