Files
DOSSIER/src/DimensionenApp.jsx
T
karim d5bcee2157 UI-Konsistenz: shared BarControls + Tabellen-Look fuer Panels
Pill-basierte Toolbar-Primitiven aus OberleisteApp extrahiert nach
src/components/BarControls.jsx — BarCombo (Dropdown), BarButton
(Icon-Button), BarToggle (Label/Icon mit Active-State), BAR_H=22.

OberleisteApp nutzt jetzt die geteilten Komponenten (Verhalten
unveraendert).

EbenenManager + GeschossManager:
- Sichtbarkeits-Toolbar: native <select> + btn-icon-sm → BarCombo
  (mit visibility-Icon links) + BarButton add/settings.
- GeschossManager Stift-Icon (edit) → Settings-Icon.
- Zeilen-Layout: eckig statt Pill (margin 0, borderRadius 0,
  3px Accent-Strip links fuer aktive Zeile), minHeight 24, gap 4,
  kompaktere Padding/Icon-Sizes — Vectorworks-naeher.

DimensionenApp:
- Welt/CPlane: 2x BarToggle statt btn-contained/outlined
- Z-Selektor: 3x BarToggle (icon-only)
- Drehen-Apply + 90°-CCW/CW: BarButton mit rotate_*-Icons (4
  Preset-Buttons -90/-45/45/90 ersetzt durch 2 schnelle 90°-Buttons
  — passt besser in die schmale Sidebar)

README aktualisiert: Runtime jetzt CPython 3.9, TextEntity-RTF-Limit
dokumentiert, BarControls + text_editor/text_create erwaehnt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 23:56:33 +02:00

344 lines
13 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flex: width ? 0 : 1, width }}>
<input
type="text"
value={text}
disabled={disabled}
onChange={(e) => 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 && <span style={{ fontSize: 10, color: 'var(--text-muted)', flexShrink: 0 }}>{suffix}</span>}
</div>
)
}
// 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 (
<div style={{
position: 'relative',
width: SIZE, height: SIZE,
border: '1px solid var(--border)',
background: 'transparent',
flexShrink: 0,
}}>
{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 (
<button
key={`${xc}-${yc}`}
onClick={() => 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)'
}}
/>
)
}))}
</div>
)
}
// Z-Referenz-Selektor (Bottom / Mid / Top) — kompakt, nur Icons.
function RefZSelector({ z, onChange }) {
return (
<div style={{ display: 'flex', gap: 3 }}>
{[
{ 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 => (
<BarToggle
key={opt.code}
icon={opt.icon}
active={z === opt.code}
onClick={() => onChange(opt.code)}
title={opt.title}
/>
))}
</div>
)
}
// Inline-Label vor einem Input — minimal, knackig
function Field({ label, children, style }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, ...style }}>
<span style={{
width: 12, fontSize: 10, fontWeight: 600,
color: 'var(--text-muted)', textAlign: 'center',
fontFamily: 'DM Mono, monospace', flexShrink: 0,
}}>
{label}
</span>
{children}
</div>
)
}
// ---- 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 (
<div style={{
display: 'flex', flexDirection: 'column',
height: '100vh', overflow: 'hidden',
background: 'var(--bg-base)', color: 'var(--text-primary)',
fontFamily: 'var(--font)', fontSize: 11,
}}>
<div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden' }}>
{/* Header: Selektions-Info + World/CPlane */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<Icon name="select_all" size={14} style={{ color: 'var(--text-muted)' }} />
<span style={{ flex: 1, fontWeight: 500 }}>{selLabel()}</span>
<div style={{ display: 'flex', gap: 3 }}>
<BarToggle
label="Welt"
active={state.coordSystem === 'world'}
onClick={() => onCoordChange('world')}
title="Weltkoordinaten"
/>
<BarToggle
label="CPlane"
active={state.coordSystem === 'cplane'}
onClick={() => onCoordChange('cplane')}
title="Aktive Konstruktionsebene"
/>
</div>
</div>
{!hasSelection ? (
<div style={{
padding: '32px 16px', textAlign: 'center',
color: 'var(--text-muted)', fontSize: 11,
}}>
<Icon name="aspect_ratio" size={32} style={{ color: 'var(--text-muted)', opacity: 0.4 }} />
<div style={{ marginTop: 8 }}>Keine Selektion.</div>
<div style={{ marginTop: 4, fontSize: 10 }}>
In Rhino ein oder mehrere Objekte auswählen.
</div>
</div>
) : (
<>
{/* Referenzpunkt */}
<div style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '10px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs" style={{ width: 30 }}>Ref</span>
<RefPointGrid ref={ref} onChange={onRefChange} />
<div style={{ flex: 1 }} />
<RefZSelector z={ref.z} onChange={(z) => onRefChange({ ...ref, z })} />
</div>
{/* Position + BBox nebeneinander */}
<div style={{
display: 'flex', gap: 16,
padding: '10px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between',
alignItems: 'baseline', marginBottom: 2 }}>
<span className="label-xs">Position</span>
<span style={{ fontFamily: 'DM Mono, monospace', fontSize: 9,
color: 'var(--text-muted)' }}>
{state.planeName}
</span>
</div>
<Field label="X"><NumInput value={pos.x} onCommit={(v) => setDimPosition('x', v)} /></Field>
<Field label="Y"><NumInput value={pos.y} onCommit={(v) => setDimPosition('y', v)} /></Field>
<Field label="Z"><NumInput value={pos.z} onCommit={(v) => setDimPosition('z', v)} /></Field>
</div>
<div style={{ width: 1, background: 'var(--border-light)' }} />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
<span className="label-xs" style={{ marginBottom: 2 }}>BBox</span>
<Field label="B"><NumInput value={dims?.width} onCommit={(v) => setDimDimension('width', v)} /></Field>
<Field label="T"><NumInput value={dims?.depth} onCommit={(v) => setDimDimension('depth', v)} /></Field>
<Field label="H"><NumInput value={dims?.height} onCommit={(v) => setDimDimension('height', v)} /></Field>
</div>
</div>
{/* Shape-spezifisch */}
{shape && (
<div style={{
display: 'flex', flexDirection: 'column', gap: 4,
padding: '10px 12px',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs" style={{ color: 'var(--accent)' }}>
{shape.type === 'circle' && 'Kreis'}
{shape.type === 'rectangle' && 'Rechteck'}
{shape.type === 'line' && 'Linie'}
</span>
{shape.type === 'circle' && (
<Field label="R"><NumInput value={shape.radius} onCommit={(v) => setCircleRadius(v)} /></Field>
)}
{shape.type === 'rectangle' && (
<div style={{ display: 'flex', gap: 8 }}>
<Field label="W" style={{ flex: 1 }}>
<NumInput value={shape.width}
onCommit={(v) => setRectangleDims(v, shape.height)} />
</Field>
<Field label="H" style={{ flex: 1 }}>
<NumInput value={shape.height}
onCommit={(v) => setRectangleDims(shape.width, v)} />
</Field>
</div>
)}
{shape.type === 'line' && (
<div style={{ display: 'flex', gap: 8 }}>
<Field label="L" style={{ flex: 1 }}>
<NumInput value={shape.length} onCommit={(v) => setLineLength(v)} />
</Field>
<Field label="α" style={{ flex: 1 }}>
<NumInput value={shape.angle} onCommit={() => {}} disabled suffix="°" />
</Field>
</div>
)}
</div>
)}
{/* Rotation */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '10px 12px',
}}>
<span className="label-xs" style={{ width: 50 }}>Drehen</span>
<div style={{ width: 56 }}>
<NumInput value={rotationDelta} onCommit={setRotationDelta} suffix="°" />
</div>
<BarButton
icon="rotate_right"
onClick={() => { if (rotationDelta) setDimRotationZ(rotationDelta) }}
disabled={!rotationDelta}
title="Selektion um Z-Achse der aktiven Plane drehen"
/>
<div style={{ flex: 1 }} />
<BarButton
icon="rotate_90_degrees_ccw"
onClick={() => setDimRotationZ(-90)}
title="90° gegen den Uhrzeigersinn"
/>
<BarButton
icon="rotate_90_degrees_cw"
onClick={() => setDimRotationZ(90)}
title="90° im Uhrzeigersinn"
/>
</div>
</>
)}
</div>
</div>
)
}