Files
web-cad/frontend/src/components/PropertiesPanel.tsx
T

218 lines
10 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { polygonArea } from '../tools/modification/geometry';
import { formatArea } from '../utils/format';
import type { PropertiesPanelProps } from '../types/ui.types';
import { formatInputValue, parseDistance, type UnitType } from '../utils/format';
import { getActiveDocument } from '../kernel/document/documentService';
const parseNum = (s: string) => parseFloat(s.replace(/[^0-9.\-]/g, '')) || 0;
const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
selectedElement,
layers,
onUpdateProperty,
onDelete,
unit = 'mm',
scaleFactor = 1,
}) => {
const el = selectedElement;
// Local input state — allows user to type freely, synced when element or unit changes
const [xInput, setXInput] = useState('');
const [yInput, setYInput] = useState('');
const [wInput, setWInput] = useState('');
const [hInput, setHInput] = useState('');
const [rotInput, setRotInput] = useState('');
const [radiusInput, setRadiusInput] = useState('');
// Sync displayed values when element changes or unit changes
useEffect(() => {
if (el) {
setXInput(formatInputValue(el.x, unit, scaleFactor));
setYInput(formatInputValue(el.y, unit, scaleFactor));
setWInput(formatInputValue(el.width, unit, scaleFactor));
setHInput(formatInputValue(el.height, unit, scaleFactor));
setRotInput(String(el.properties.rotation ?? 0));
const radius = el.properties.radius as number | undefined;
setRadiusInput(radius !== undefined ? formatInputValue(radius, unit, scaleFactor) : '');
}
}, [el, unit, scaleFactor]);
const color = (el?.properties.stroke as string) || '#3b82f6';
const blockName = el?.properties.blockId || '';
const desc = (el?.properties.text as string) || '';
const handleXChange = (val: string) => {
setXInput(val);
const world = parseDistance(val, unit, scaleFactor);
onUpdateProperty('x', world);
};
const handleYChange = (val: string) => {
setYInput(val);
const world = parseDistance(val, unit, scaleFactor);
onUpdateProperty('y', world);
};
const handleWChange = (val: string) => {
setWInput(val);
const world = parseDistance(val, unit, scaleFactor);
onUpdateProperty('width', world);
};
const handleHChange = (val: string) => {
setHInput(val);
const world = parseDistance(val, unit, scaleFactor);
onUpdateProperty('height', world);
};
const handleRadiusChange = (val: string) => {
setRadiusInput(val);
const world = parseDistance(val, unit, scaleFactor);
onUpdateProperty('radius', world);
};
return (
<>
<div className="panel-section">
<div className="panel-section-title">
<span>Auswahl {el ? `· ${el.type}
{((el.type === 'polygon' || el.type === 'polyline') && ((el.properties.points as Array<{ x: number; y: number }> | undefined)?.length ?? 0) >= 3) && (
<div className="prop-row">
<label className="prop-label">Fläche</label>
<span className="prop-value">{formatArea(polygonArea(el.properties.points as Array<{ x: number; y: number }>), unit, scaleFactor)}</span>
</div>
)}
{/* ─── Task D12: Block-Attribute-Formular (block_instance) ─── */}
{el.type === 'block_instance' && (() => {
const blockId = el.properties.blockId as string | undefined;
if (!blockId) return null;
const block = getActiveDocument()?.getBlock(blockId);
const attrDefs = block?.attrDefs ?? [];
if (attrDefs.length === 0) return null;
const attrValues = (el.properties.attrValues as Record<string, string> | undefined) ?? {};
return (
<div className="panel-section">
<div className="panel-section-title">Block-Attribute</div>
{attrDefs.map((def) => (
<div className="prop-row" key={def.tag}>
<label className="prop-label" title={def.prompt}>{def.tag}</label>
<input
className="prop-input"
type="text"
value={attrValues[def.tag] ?? def.defaultValue ?? ''}
placeholder={def.prompt}
onChange={(ev) => {
const next = { ...attrValues, [def.tag]: ev.target.value };
onUpdateProperty('attrValues', next);
}}
/>
</div>
))}
</div>
);
})()}` : ''}</span>
<div style={{ display: 'flex', gap: '4px' }}>
{onDelete && el && (
<button
onClick={onDelete}
title="Auswahl löschen (Entf)"
aria-label="Auswahl löschen"
style={{ color: '#f44336', background: 'none', border: 'none', cursor: 'pointer', padding: '2px', display: 'flex', alignItems: 'center' }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
)}
<button style={{ color: 'var(--color-text-muted)', fontSize: '11px', background: 'none', border: 'none', cursor: 'pointer', padding: '2px' }} title="Mehr Optionen">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
</button>
</div>
</div>
</div>
<div className="panel-section">
<div className="panel-section-title"><span>Geometrie</span></div>
<div className="prop-row">
<span className="prop-label">Position X ({unit})</span>
<input className="prop-input" type="text" value={xInput} aria-label="Position X" onChange={(e) => handleXChange(e.target.value)} />
</div>
<div className="prop-row">
<span className="prop-label">Position Y ({unit})</span>
<input className="prop-input" type="text" value={yInput} aria-label="Position Y" onChange={(e) => handleYChange(e.target.value)} />
</div>
<div className="prop-row">
<span className="prop-label">Breite ({unit})</span>
<input className="prop-input" type="text" value={wInput} aria-label="Breite" onChange={(e) => handleWChange(e.target.value)} />
</div>
<div className="prop-row">
<span className="prop-label">Tiefe ({unit})</span>
<input className="prop-input" type="text" value={hInput} aria-label="Tiefe" onChange={(e) => handleHChange(e.target.value)} />
</div>
{el?.type === 'circle' && (
<div className="prop-row">
<span className="prop-label">Radius ({unit})</span>
<input className="prop-input" type="text" value={radiusInput} aria-label="Radius" onChange={(e) => handleRadiusChange(e.target.value)} />
</div>
)}
<div className="prop-row">
<span className="prop-label">Drehung (°)</span>
<input className="prop-input" type="text" value={rotInput} aria-label="Drehung" onChange={(e) => { setRotInput(e.target.value); onUpdateProperty('rotation', parseNum(e.target.value)); }} />
</div>
</div>
<div className="panel-section">
<div className="panel-section-title"><span>Darstellung</span></div>
<div className="prop-row">
<span className="prop-label">Farbe</span>
<div className="prop-color-row">
<div className="prop-color-swatch" style={{ background: color }} aria-hidden="true"></div>
<input className="prop-swatch-input" type="color" value={color} aria-label="Elementfarbe" onChange={(e) => onUpdateProperty('stroke', e.target.value)} />
<input className="prop-input" type="text" value={color.toUpperCase()} aria-label="Farb-Hex" onChange={(e) => onUpdateProperty('stroke', e.target.value)} />
</div>
</div>
<div className="prop-row">
<span className="prop-label">Linientyp</span>
<select className="prop-select" aria-label="Linientyp" value={el?.properties.lineType || 'solid'} onChange={(e) => onUpdateProperty('lineType', e.target.value)}>
<option value="solid">Solid (durchgehend)</option>
<option value="dashed">Dashed (gestrichelt)</option>
<option value="dotted">Dotted (gepunktet)</option>
<option value="dash-dot">Dash-Dot</option>
</select>
</div>
<div className="prop-row">
<span className="prop-label">Stärke</span>
<select className="prop-select" aria-label="Linienstärke" value={String(el?.properties.strokeWidth ?? '1')} onChange={(e) => onUpdateProperty('strokeWidth', parseNum(e.target.value))}>
<option value="0.18">0.18 mm · Normal</option>
<option value="0.25">0.25 mm · Dick</option>
<option value="0.35">0.35 mm · Extra</option>
<option value="0.50">0.50 mm · Max</option>
</select>
</div>
<div className="prop-row">
<span className="prop-label">Layer</span>
<select className="prop-select" aria-label="Layer zuweisen" value={el?.layerId || ''} onChange={(e) => onUpdateProperty('layerId', e.target.value)}>
{layers.length > 0 ? layers.map((l) => (
<option key={l.id} value={l.id}>{l.name}</option>
)) : (
<option value="">Kein Layer</option>
)}
</select>
</div>
</div>
{el?.type === 'block_instance' && (
<div className="panel-section">
<div className="panel-section-title"><span>Block</span></div>
<div className="prop-row">
<span className="prop-label">Block-Name</span>
<input className="prop-input" type="text" value={blockName} aria-label="Block-Name" onChange={(e) => onUpdateProperty('blockId', e.target.value)} />
</div>
<div className="prop-row">
<span className="prop-label">Beschreibung</span>
<input className="prop-input" type="text" value={desc} aria-label="Beschreibung" onChange={(e) => onUpdateProperty('text', e.target.value)} />
</div>
</div>
)}
</>
);
};
export default PropertiesPanel;