Files
DOSSIER/src/DimensionenApp.jsx
T
karim 95031ee2c0 Panels poliert: Ebenenkombi in Oberleiste, Satelliten-Dialoge, Caps weg, Perf
- Ebenenkombination raus aus Ebenen-Panel, in Oberleiste-Topbar +
  Editor-Satellite (AusschnittLayerDialog embedded). doc.Strings
  haelt active_comb_name, auto-clear bei manueller Eye/Lock-Aenderung.
- EbenenSettingsDialog jetzt Satellite mit Ebene-Picker-Dropdown
  (auto-save on switch via SAVE_KEEP).
- Per-Ausschnitt Einstellungen-Satellite (Massstab, Display, Overrides,
  Ebenenkombi). Alte 'Sichtbarkeit bearbeiten'-Option entfernt.
- Layouts/Ausschnitte: Top-Header weg, Sticky-Footer mit Anzahl +
  Aktionen. LayoutDialog ist jetzt Satellite mit Format-Live-Preview.
- Panel-Captions + Default-Ebenen-Namen auf Mixed-Case (Ausschnitte,
  Ebenen, Waende ...). Nur DOSSIER bleibt caps.
- DimensionenApp: Card-Optik raus, REF-Wuerfel mit Kreisen statt
  Quadraten + Hover-Scale.
- GeschossManager angeglichen an EbenenManager: Rechtsklick-Menue,
  Lock-Button, Delete-X, Duplizieren. layer_builder honoriert z.locked.
- Active Sublayer folgt jetzt dem Geschoss-Wechsel (gleicher Code
  unter neuem Parent).

Performance Geschoss-Wechsel:
- elemente._send_state() ersetzt durch _notify_active_geschoss()
  (Partial-Push statt 200+ Elements re-enumerieren).
- _apply_visibility dedupe via sticky last-applied-signature
  (STATE_SYNC-Echo loopt nicht mehr durch alle Layer).
- _update_clipping nur wenn alt oder neu hasClipping=True.
- Redundante doc.Views.Redraw() im CPlane-Pfad entfernt — die folgende
  apply_visibility-Roundtrip redrawt 30ms spaeter ohnehin.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 03:58:28 +02:00

352 lines
14 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 {
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: 2 }}>
{[
{ 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 => (
<button
key={opt.code}
onClick={() => onChange(opt.code)}
className={z === opt.code ? 'btn-contained' : 'btn-outlined'}
style={{
padding: '2px 5px', fontSize: 10,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
title={opt.title}
>
<Icon name={opt.icon} size={12} />
</button>
))}
</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: 2 }}>
<button
onClick={() => onCoordChange('world')}
className={state.coordSystem === 'world' ? 'btn-contained' : 'btn-outlined'}
style={{ fontSize: 10, padding: '3px 8px' }}
title="Weltkoordinaten"
>Welt</button>
<button
onClick={() => onCoordChange('cplane')}
className={state.coordSystem === 'cplane' ? 'btn-contained' : 'btn-outlined'}
style={{ fontSize: 10, padding: '3px 8px' }}
title="Aktive Konstruktionsebene"
>CPlane</button>
</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>
<button
className="btn-outlined"
onClick={() => { if (rotationDelta) setDimRotationZ(rotationDelta) }}
disabled={!rotationDelta}
title="Selektion um Z-Achse der aktiven Plane drehen"
style={{ padding: '3px 8px', fontSize: 11 }}
>
<Icon name="rotate_right" size={13} />
</button>
<div style={{ flex: 1 }} />
{[-90, -45, 45, 90].map(a => (
<button
key={a}
className="btn-outlined"
onClick={() => setDimRotationZ(a)}
style={{ padding: '3px 6px', fontSize: 9, minWidth: 28 }}
title={`${a}°`}
>
{a > 0 ? '+' : ''}{a}°
</button>
))}
</div>
</>
)}
</div>
</div>
)
}