import { useEffect, useState, useRef } from 'react'
import Icon from './components/Icon'
import { BarToggle, BarButton } from './components/BarControls'
import {
onMessage, notifyReady,
setRefPoint, setCoordSystem,
setDimPosition, setDimDimension, setDimRotationZ,
setCircleRadius, setLineLength, setRectangleDims,
} from './lib/rhinoBridge'
// ---- Helpers --------------------------------------------------------------
const REF_CODES = ['min', 'mid', 'max'] // row/col mapping
const REF_LABELS = { min: 'min', mid: 'mid', max: 'max' }
function fmtNum(v) {
if (v == null) return ''
if (typeof v !== 'number') return String(v)
// 4 Nachkommastellen, aber unnoetige Nullen weg
return Number(v.toFixed(4)).toString()
}
// Input-Komponente: zeigt formatierten Wert, sendet onCommit bei Enter/Blur.
// Verhindert Update waehrend des Tippens, damit der Cursor nicht springt.
function NumInput({ value, onCommit, disabled, suffix, width }) {
const [text, setText] = useState(fmtNum(value))
const [focused, setFocused] = useState(false)
useEffect(() => { if (!focused) setText(fmtNum(value)) }, [value, focused])
const commit = () => {
const v = parseFloat(text.replace(',', '.'))
if (!isNaN(v) && v !== value) onCommit(v)
else setText(fmtNum(value)) // ungueltig → zurueck auf alten Wert
}
return (
setText(e.target.value)}
onFocus={(e) => { setFocused(true); e.target.select() }}
onBlur={() => { setFocused(false); commit() }}
onKeyDown={(e) => {
if (e.key === 'Enter') { e.target.blur() }
else if (e.key === 'Escape') { setText(fmtNum(value)); e.target.blur() }
}}
style={{ flex: 1, width: '100%', fontFamily: 'DM Mono, monospace', fontSize: 11, textAlign: 'right' }}
/>
{suffix && {suffix} }
)
}
// 9-Punkt-Referenzpunkt-Selektor: sichtbarer BBox-Rahmen, Kreise auf den
// Eckpunkten / Kantenmitten / Zentrum.
function RefPointGrid({ ref, onChange }) {
const SIZE = 28 // Aussenkanten-Quadrat (px)
const DOT = 6 // Kreis-Durchmesser (px)
const pct = (c) => c === 'min' ? '0%' : c === 'max' ? '100%' : '50%'
return (
{REF_CODES.map(yc => REF_CODES.map(xc => {
const active = ref.x === xc && ref.y === yc
const topPct = yc === 'max' ? '0%' : yc === 'min' ? '100%' : '50%'
return (
onChange({ ...ref, x: xc, y: yc })}
title={`X=${xc}, Y=${yc}`}
style={{
position: 'absolute',
left: pct(xc), top: topPct,
transform: 'translate(-50%, -50%)',
width: DOT, height: DOT, padding: 0,
borderRadius: '50%',
background: active ? 'var(--accent)' : 'var(--text-muted)',
border: 'none',
cursor: 'pointer',
transition: 'background 0.12s, transform 0.12s',
}}
onMouseEnter={(e) => {
if (!active) e.currentTarget.style.background = 'var(--text-primary)'
e.currentTarget.style.transform = 'translate(-50%, -50%) scale(1.25)'
}}
onMouseLeave={(e) => {
if (!active) e.currentTarget.style.background = 'var(--text-muted)'
e.currentTarget.style.transform = 'translate(-50%, -50%) scale(1)'
}}
/>
)
}))}
)
}
// Z-Referenz-Selektor (Bottom / Mid / Top) — kompakt, nur Icons.
function RefZSelector({ z, onChange }) {
return (
{[
{ code: 'max', icon: 'vertical_align_top', title: 'Z = Top' },
{ code: 'mid', icon: 'vertical_align_center', title: 'Z = Mid' },
{ code: 'min', icon: 'vertical_align_bottom', title: 'Z = Bottom' },
].map(opt => (
onChange(opt.code)}
title={opt.title}
/>
))}
)
}
// Inline-Label vor einem Input — minimal, knackig
function Field({ label, children, style }) {
return (
{label}
{children}
)
}
// ---- Hauptkomponente ------------------------------------------------------
export default function DimensionenApp() {
const [state, setState] = useState({
selection: { count: 0, type: 'none' },
refPoint: { x: 'min', y: 'min', z: 'mid' },
coordSystem: 'world',
position: null,
dimensions: null,
shape: null,
planeName: 'Welt',
})
const [rotationDelta, setRotationDelta] = useState(0)
useEffect(() => {
onMessage('STATE', (s) => setState((prev) => ({ ...prev, ...s })))
notifyReady()
}, [])
const sel = state.selection || { count: 0, type: 'none' }
const ref = state.refPoint || { x: 'min', y: 'min', z: 'mid' }
const pos = state.position
const dims = state.dimensions
const shape = state.shape
const hasSelection = sel.count > 0 && pos != null
const onRefChange = (next) => setRefPoint(next.x, next.y, next.z)
const onCoordChange = (mode) => setCoordSystem(mode)
// Selektions-Beschriftung
const selLabel = () => {
if (sel.count === 0) return 'Keine Selektion'
if (sel.count === 1) {
const map = {
curve: 'Kurve', brep: 'Brep', mesh: 'Mesh', extrusion: 'Extrusion',
block: 'Block', point: 'Punkt', text: 'Text', other: 'Objekt',
}
let base = map[sel.type] || '1 Objekt'
if (shape?.type === 'circle') base = 'Kreis'
if (shape?.type === 'rectangle') base = 'Rechteck'
if (shape?.type === 'line') base = 'Linie'
return '1 ' + base
}
return `${sel.count} Objekte`
}
return (
{/* Header: Selektions-Info + World/CPlane */}
{selLabel()}
onCoordChange('world')}
title="Weltkoordinaten"
/>
onCoordChange('cplane')}
title="Aktive Konstruktionsebene"
/>
{!hasSelection ? (
Keine Selektion.
In Rhino ein oder mehrere Objekte auswählen.
) : (
<>
{/* Referenzpunkt */}
Ref
onRefChange({ ...ref, z })} />
{/* Position + BBox nebeneinander */}
Position
{state.planeName}
setDimPosition('x', v)} />
setDimPosition('y', v)} />
setDimPosition('z', v)} />
BBox
setDimDimension('width', v)} />
setDimDimension('depth', v)} />
setDimDimension('height', v)} />
{/* Shape-spezifisch */}
{shape && (
{shape.type === 'circle' && 'Kreis'}
{shape.type === 'rectangle' && 'Rechteck'}
{shape.type === 'line' && 'Linie'}
{shape.type === 'circle' && (
setCircleRadius(v)} />
)}
{shape.type === 'rectangle' && (
setRectangleDims(v, shape.height)} />
setRectangleDims(shape.width, v)} />
)}
{shape.type === 'line' && (
setLineLength(v)} />
{}} disabled suffix="°" />
)}
)}
{/* Rotation */}
Drehen
{ if (rotationDelta) setDimRotationZ(rotationDelta) }}
disabled={!rotationDelta}
title="Selektion um Z-Achse der aktiven Plane drehen"
/>
setDimRotationZ(-90)}
title="90° gegen den Uhrzeigersinn"
/>
setDimRotationZ(90)}
title="90° im Uhrzeigersinn"
/>
>
)}
)
}