import { useState, useRef, useCallback, useEffect } from 'react'; import { api, S } from '../lib.js'; // ── Hilfsfunktionen ────────────────────────────────────────────────────────── const uid = () => Math.random().toString(36).slice(2,8); const dist = (x1,y1,x2,y2) => Math.sqrt((x2-x1)**2+(y2-y1)**2); const snapToGrid = (v, g) => Math.round(v / g) * g; function parseDim(s) { if (!s) return null; const n = parseFloat(String(s).replace(/[^0-9.,\-]/g,'').replace(',','.')); return isNaN(n) ? null : n; } function evalFormula(expr, vars) { try { const sanitized = expr.replace(/[^0-9+\-*/().,%a-zA-Z_\s]/g,''); const keys = Object.keys(vars), vals = keys.map(k=>vars[k]); // eslint-disable-next-line no-new-func return new Function(...keys, `"use strict"; return (${sanitized});`)(...vals); } catch { return null; } } function buildVars(shapes) { const vars = {}; shapes.forEach(s => { const n = (s.name||'').replace(/[^a-zA-Z0-9_]/g,'_'); if (!n) return; if (s.type==='rect') { const l=parseDim(s.dimW), b=parseDim(s.dimH); if (l!=null) { vars[`${n}_l`]=l; vars[`${n}_b`]=b??l; } if (l!=null&&b!=null) { vars[`${n}_fl`]=+(l*b).toFixed(4); vars[`${n}_um`]=+(2*(l+b)).toFixed(4); vars[`${n}_d`]=+(Math.sqrt(l*l+b*b)).toFixed(4); } } if (s.type==='line') { const v=parseDim(s.dimLen); if(v!=null) vars[`${n}_len`]=v; } if (s.type==='circle') { const r=parseDim(s.dimR); if(r!=null){ vars[`${n}_r`]=r; vars[`${n}_d`]=+(r*2).toFixed(4); vars[`${n}_fl`]=+(Math.PI*r*r).toFixed(4); vars[`${n}_um`]=+(2*Math.PI*r).toFixed(4); } } }); return vars; } // ── Row-Komponente (AUSSERHALB um Input-Bug zu vermeiden) ───────────────────── function DimRow({ label, value, field, placeholder, onChange }) { return (
{label}
onChange({[field]:e.target.value})} placeholder={placeholder} autoCapitalize="none" style={{...S.inp,fontSize:12,padding:'6px 8px'}}/>
); } // ── Maßlinie ───────────────────────────────────────────────────────────────── function DimLine({x1,y1,x2,y2,label,color='#4ecdc4',offset=18}) { const mx=(x1+x2)/2, my=(y1+y2)/2; const dx=x2-x1, dy=y2-y1, len=Math.sqrt(dx*dx+dy*dy); if (len<4) return null; const nx=-dy/len*offset, ny=dx/len*offset; return ( {label} ); } // ── Shape + Mittelpunkt + Versatz ───────────────────────────────────────────── function Shape({ s, selected, onClick }) { const sel = selected===s.id; const c = sel ? '#4ecdc4' : (s.color||'#7ecaff'); const offset = parseDim(s.offset) || 0; // Versatz in px (scaled) if (s.type==='rect') { const x=Math.min(s.x,s.x+s.w), y=Math.min(s.y,s.y+s.h); const w=Math.abs(s.w), h=Math.abs(s.h); const cx=x+w/2, cy=y+h/2; const offPx = s.offsetPx||0; return ( {e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}> {/* Mittelpunkt */} {/* Versatz */} {offPx!==0&&0?'#ffe66d':'#ff6b9d'} strokeWidth={1} strokeDasharray="4,3" opacity={0.7}/>} {s.name&&{s.name}} {s.dimW&&} {s.dimH&&} {sel&&<> } ); } if (s.type==='line') { const len=dist(s.x1,s.y1,s.x2,s.y2); const mx=(s.x1+s.x2)/2, my=(s.y1+s.y2)/2; return ( {e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}> {(s.dimLen||s.name)&&len>20&&} {sel&&<>} ); } if (s.type==='circle') { const r=Math.max(1,s.r); const offPx=s.offsetPx||0; return ( {e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}> {offPx!==0&&0?'#ffe66d':'#ff6b9d'} strokeWidth={1} strokeDasharray="4,3" opacity={0.7}/>} {/* Mittelpunkt */} {/* Radiuslinie */} {s.name&&{s.name}} {s.dimR&&} {sel&&} ); } if (s.type==='text') { return ( {e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}> {s.label||'Text'} {sel&&} ); } return null; } // ── Properties Panel ────────────────────────────────────────────────────────── function PropsPanel({ shape, onChange, onDelete, vars, scale, onScaleChange }) { if (!shape) return (
Form auswählen oder zeichnen.
V=Auswählen R=Rect L=Linie C=Kreis
); const shapeVarKeys = Object.keys(vars).filter(k=>k.startsWith((shape.name||'').replace(/[^a-zA-Z0-9_]/g,'_')+'_')); return (
EIGENSCHAFTEN
{shape.type==='rect'&&<> } {shape.type==='line'&&} {shape.type==='circle'&&<> } {shape.type==='text'&&} {shapeVarKeys.length>0&&(
VARIABLEN
{shapeVarKeys.map(k=>(
{k} {typeof vars[k]==='number'?vars[k].toFixed(2):vars[k]}
))}
)}
MASSSTAB (mm/px)
onScaleChange(parseFloat(e.target.value)||1)} step={0.1} min={0.1} style={{...S.inp,flex:1,fontSize:12,padding:'5px 8px'}}/> mm/px
); } // ── Main ────────────────────────────────────────────────────────────────────── const TOOLS = [ {id:'select',label:'▲',title:'Auswählen (V)'}, {id:'rect', label:'▭',title:'Rechteck (R)'}, {id:'line', label:'╱',title:'Linie (L)'}, {id:'circle',label:'○',title:'Kreis (C)'}, {id:'text', label:'T', title:'Text (T)'}, ]; const GRID = 10; const CALC_BTNS = ['(',')',' ','7','8','9','×','4','5','6','÷','1','2','3','-','0','.','=','+']; export default function Skizze({ toast, mobile }) { const [shapes, setShapes] = useState([]); const [selected, setSelected] = useState(null); const [tool, setTool] = useState('select'); const [drawing, setDrawing] = useState(null); const [formula, setFormula] = useState(''); const [results, setResults] = useState([]); const [snapGrid, setSnapGrid] = useState(true); const [scale, setScale] = useState(1); // mm per pixel const svgRef = useRef(null); const formulaRef= useRef(null); const vars = buildVars(shapes); const selShape = shapes.find(s=>s.id===selected)||null; // Keyboard shortcuts useEffect(()=>{ const h = e => { if (e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA') return; const map={v:'select',r:'rect',l:'line',c:'circle',t:'text'}; if (map[e.key.toLowerCase()]) setTool(map[e.key.toLowerCase()]); if ((e.key==='Delete'||e.key==='Backspace')&&selected) deleteSelected(); if (e.key==='Escape') { setSelected(null); setDrawing(null); } }; window.addEventListener('keydown',h); return()=>window.removeEventListener('keydown',h); },[selected]); const getSVGPoint = useCallback(e=>{ const rect=svgRef.current?.getBoundingClientRect(); if (!rect) return{x:0,y:0}; const cx=e.touches?e.touches[0].clientX:e.clientX; const cy=e.touches?e.touches[0].clientY:e.clientY; const x=cx-rect.left, y=cy-rect.top; return {x:snapGrid?snapToGrid(x,GRID):x, y:snapGrid?snapToGrid(y,GRID):y}; },[snapGrid]); const onSVGDown = useCallback(e=>{ if (tool==='select'){setSelected(null);return;} const pt=getSVGPoint(e); if (tool==='text'){ setShapes(p=>[...p,{id:uid(),type:'text',x:pt.x,y:pt.y,label:'Text',name:''}]); setTool('select'); return; } setDrawing({tool,startX:pt.x,startY:pt.y,curX:pt.x,curY:pt.y}); },[tool,getSVGPoint]); const onSVGMove = useCallback(e=>{ if (!drawing) return; const pt=getSVGPoint(e); setDrawing(d=>({...d,curX:pt.x,curY:pt.y})); },[drawing,getSVGPoint]); const onSVGUp = useCallback(e=>{ if (!drawing) return; const pt=getSVGPoint(e); const dx=pt.x-drawing.startX, dy=pt.y-drawing.startY; if (Math.abs(dx)<4&&Math.abs(dy)<4){setDrawing(null);return;} let s; if (drawing.tool==='rect') s={id:uid(),type:'rect',x:drawing.startX,y:drawing.startY,w:dx,h:dy,name:'',dimW:'',dimH:'',dimOffset:''}; if (drawing.tool==='line') s={id:uid(),type:'line',x1:drawing.startX,y1:drawing.startY,x2:pt.x,y2:pt.y,name:'',dimLen:''}; if (drawing.tool==='circle') s={id:uid(),type:'circle',cx:drawing.startX,cy:drawing.startY,r:dist(drawing.startX,drawing.startY,pt.x,pt.y),name:'',dimR:'',dimOffset:''}; if (s){setShapes(p=>[...p,s]);setSelected(s.id);} setDrawing(null); },[drawing,getSVGPoint]); const deleteSelected = ()=>{setShapes(p=>p.filter(s=>s.id!==selected));setSelected(null);}; // Update shape + auto-resize visual proportions based on scale const updateShape = patch => { setShapes(p=>p.map(s=>{ if (s.id!==selected) return s; const upd={...s,...patch}; // Auto-resize rect if (upd.type==='rect') { const mW=parseDim(upd.dimW), mH=parseDim(upd.dimH); if (mW!=null&&mW>0) { upd.w=Math.sign(upd.w||1)*mW/scale; if (mH!=null&&mH>0) upd.h=Math.sign(upd.h||1)*mH/scale; } // Versatz in px const mO=parseDim(upd.dimOffset); upd.offsetPx=mO!=null?mO/scale:0; } // Auto-resize circle if (upd.type==='circle') { const mR=parseDim(upd.dimR); if (mR!=null&&mR>0) upd.r=mR/scale; const mO=parseDim(upd.dimOffset); upd.offsetPx=mO!=null?mO/scale:0; } return upd; })); }; const calcFormula = () => { if (!formula.trim()) return; const result=evalFormula(formula,vars); const isArea=formula.toLowerCase().includes('_fl'); setResults(p=>[{ id:uid(), expr:formula, result:result!=null?`${typeof result==='number'?result.toFixed(4).replace(/.?0+$/,''):result}${isArea?' mm²':' mm'}`:null, },...p].slice(0,30)); }; const insertCalc = btn => { if (btn==='='){calcFormula();return;} const map={'×':'*','÷':'/',' ':' '}; const char=map[btn]??btn; setFormula(f=>f+char); formulaRef.current?.focus(); }; // Preview while drawing let preview=null; if (drawing){ const dx=drawing.curX-drawing.startX, dy=drawing.curY-drawing.startY; const pc='#4ecdc4'; if (drawing.tool==='rect') preview=; if (drawing.tool==='line') preview=; if (drawing.tool==='circle'){const r=dist(drawing.startX,drawing.startY,drawing.curX,drawing.curY); preview=;} } const canvasH = mobile ? 260 : 420; return (

Skizzen-Rechner

🚧 IM AUFBAU
{/* Toolbar */}
{TOOLS.map(t=>( ))} {TOOLS.find(t=>t.id===tool)?.title}
{/* Canvas + Panel */}
{snapGrid&&} {shapes.map(s=>{if(tool==='select')setSelected(id);}}/>)} {preview}
{/* Rechner */}
RECHNER
{results.length>0&&}
{/* Alle Variablen */} {Object.keys(vars).length>0&&(
{Object.entries(vars).map(([k,v])=>( {setFormula(f=>f+k);formulaRef.current?.focus();}}> {k} ={typeof v==='number'?v.toFixed(2):v} ))}
)} {/* Input */}
setFormula(e.target.value)} onKeyDown={e=>e.key==='Enter'&&calcFormula()} placeholder="z.B. Kasten_l / 2" autoCapitalize="none" style={{...S.inp,flex:1,fontFamily:"'Courier New',monospace",fontSize:13}}/>
{/* Operator-Buttons */}
{['(',')',' × ',' ÷ ',' '].map((b,i)=>( b.trim()===''?
: ))}
{/* Ergebnisse */} {results.length===0&&Object.keys(vars).length===0&&(
Formen zeichnen → benennen → hier rechnen
)} {results.map(r=>(
{r.expr}
{r.result||'❌ Fehler'}
))}
); }