diff --git a/frontend/src/tools/skizze.jsx b/frontend/src/tools/skizze.jsx index 6e6b44a..3512712 100644 --- a/frontend/src/tools/skizze.jsx +++ b/frontend/src/tools/skizze.jsx @@ -1,94 +1,103 @@ -import { useState, useRef, useCallback, useEffect, useId } from 'react'; +import { useState, useRef, useCallback, useEffect } from 'react'; import { api, S } from '../lib.js'; // ── Hilfsfunktionen ────────────────────────────────────────────────────────── const uid = () => Math.random().toString(36).slice(2,8); -const clamp = (v,a,b) => Math.max(a, Math.min(b, v)); -const dist = (x1,y1,x2,y2) => Math.sqrt((x2-x1)**2+(y2-y1)**2); +const dist = (x1,y1,x2,y2) => Math.sqrt((x2-x1)**2+(y2-y1)**2); +const snapToGrid = (v, g) => Math.round(v / g) * g; -function snapToGrid(v, grid) { return grid ? Math.round(v / grid) * grid : v; } - -// Parse dimension string "225mm" → 225, or just "225" → 225 function parseDim(s) { if (!s) return null; - const n = parseFloat(String(s).replace(/[^0-9.,]/g,'').replace(',','.')); + const n = parseFloat(String(s).replace(/[^0-9.,\-]/g,'').replace(',','.')); return isNaN(n) ? null : n; } -// Evaluate formula safely, variables = { name: value } function evalFormula(expr, vars) { try { const sanitized = expr.replace(/[^0-9+\-*/().,%a-zA-Z_\s]/g,''); - const keys = Object.keys(vars); - const vals = keys.map(k => vars[k]); + 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; } } -// Build variable map from shapes function buildVars(shapes) { const vars = {}; shapes.forEach(s => { - const n = s.name?.replace(/[^a-zA-Z0-9_]/g,'_') || `_${s.id}`; - if (s.type === 'rect') { - const l = parseDim(s.dimW); const 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; vars[`${n}_um`] = 2*(l+b); } + 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 len = parseDim(s.dimLen); - if (len != null) vars[`${n}_len`] = len; - } - if (s.type === 'circle') { - const r = parseDim(s.dimR); - if (r != null) { vars[`${n}_r`] = r; vars[`${n}_d`] = r*2; vars[`${n}_fl`] = Math.PI*r*r; vars[`${n}_um`] = 2*Math.PI*r; } + 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; } -// ── DimensionLine Component ─────────────────────────────────────────────────── -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; - const len = Math.sqrt(dx*dx+dy*dy); +// ── 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; - const ax1=x1+nx, ay1=y1+ny, ax2=x2+nx, ay2=y2+ny; + const nx=-dy/len*offset, ny=dx/len*offset; return ( - - - + + + {label} + fontSize={9} fill={color} fontFamily="'Courier New',monospace" style={{userSelect:'none'}} + stroke="#0a0a0c" strokeWidth={3} paintOrder="stroke">{label} ); } -// ── Shape renderer ──────────────────────────────────────────────────────────── +// ── Shape + Mittelpunkt + Versatz ───────────────────────────────────────────── function Shape({ s, selected, onClick }) { - const sel = selected === s.id; - const c = sel ? '#4ecdc4' : (s.color || '#7ecaff'); + 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); + 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'}}> + {e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}> - {s.name && - {s.name} - } - {s.dimW && } - {s.dimH && } - {sel && <> + {/* Mittelpunkt */} + + + {/* Versatz */} + {offPx!==0&&0?'#ffe66d':'#ff6b9d'} strokeWidth={1} strokeDasharray="4,3" opacity={0.7}/>} + {s.name&&{s.name}} + {s.dimW&&} + {s.dimH&&} + {sel&&<> @@ -98,123 +107,123 @@ function Shape({ s, selected, onClick }) { ); } - if (s.type === 'line') { - const len = dist(s.x1,s.y1,s.x2,s.y2); + 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 && <> - - - } + {e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}> + + + + {(s.dimLen||s.name)&&len>20&&} + {sel&&<>} ); } - if (s.type === 'circle') { + 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'}}> - - {s.name && - {s.name} - } - {s.dimR && } - {sel && } + {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') { + if (s.type==='text') { return ( - { e.stopPropagation(); onClick(s.id); }} style={{cursor:'pointer'}}> - - {s.label||'Text'} - - {sel && {e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}> + {s.label||'Text'} + {sel&&} ); } - return null; } // ── Properties Panel ────────────────────────────────────────────────────────── -function PropsPanel({ shape, onChange, onDelete, vars }) { +function PropsPanel({ shape, onChange, onDelete, vars, scale, onScaleChange }) { if (!shape) return ( -
- Form auswählen oder zeichnen +
+ Form auswählen oder zeichnen.
+ V=Auswählen R=Rect L=Linie C=Kreis
); - const Row = ({label, value, field, placeholder}) => ( -
-
{label}
- onChange({[field]:e.target.value})} - placeholder={placeholder} autoCapitalize="none" - style={{...S.inp,fontSize:12,padding:'6px 8px'}}/> -
- ); + const shapeVarKeys = Object.keys(vars).filter(k=>k.startsWith((shape.name||'').replace(/[^a-zA-Z0-9_]/g,'_')+'_')); return (
EIGENSCHAFTEN
- - {shape.type==='rect' && <> - - + + {shape.type==='rect'&&<> + + + } - {shape.type==='line' && } - {shape.type==='circle' && } - {shape.type==='text' && } + {shape.type==='line'&&} + {shape.type==='circle'&&<> + + + } + {shape.type==='text'&&} + - {/* Variablen für diese Form */} - {shape.name && ( -
-
VERFÜGBARE VARIABLEN
- {Object.entries(vars) - .filter(([k])=>k.startsWith((shape.name||'').replace(/[^a-zA-Z0-9_]/g,'_')+'_')) - .map(([k,v])=>( -
- {k} - - {typeof v==='number'?v.toFixed(2):v} - -
- ))} + {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 Component ──────────────────────────────────────────────────────────── -const TOOLS_LIST = [ - { 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)' }, +// ── 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([]); @@ -224,257 +233,249 @@ export default function Skizze({ toast, mobile }) { const [formula, setFormula] = useState(''); const [results, setResults] = useState([]); const [snapGrid, setSnapGrid] = useState(true); - const [sketches, setSketches] = useState([]); - const [sketchName, setSketchName] = useState('Skizze 1'); - const [currentId, setCurrentId] = useState(null); - const svgRef = useRef(null); - const formulaRef = useRef(null); + 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; + 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; - if (e.key==='v'||e.key==='V') setTool('select'); - if (e.key==='r'||e.key==='R') setTool('rect'); - if (e.key==='l'||e.key==='L') setTool('line'); - if (e.key==='c'||e.key==='C') setTool('circle'); - if (e.key==='t'||e.key==='T') setTool('text'); - if (e.key==='Delete'||e.key==='Backspace') { if(selected) deleteSelected(); } + 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, shapes]); + return()=>window.removeEventListener('keydown',h); + },[selected]); - const getSVGPoint = useCallback((e) => { - const rect = svgRef.current?.getBoundingClientRect(); - if (!rect) return {x:0,y:0}; - const clientX = e.touches ? e.touches[0].clientX : e.clientX; - const clientY = e.touches ? e.touches[0].clientY : e.clientY; - const x = clientX - rect.left; - const y = clientY - rect.top; - return { - x: snapGrid ? snapToGrid(x, GRID) : x, - y: snapGrid ? snapToGrid(y, GRID) : y, - }; + 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 onSVGMouseDown = useCallback((e) => { - if (tool === 'select') { setSelected(null); return; } - const pt = getSVGPoint(e); - if (tool === 'text') { + 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; + setTool('select'); return; } - setDrawing({ tool, startX:pt.x, startY:pt.y, curX:pt.x, curY:pt.y }); - },[tool, getSVGPoint]); + setDrawing({tool,startX:pt.x,startY:pt.y,curX:pt.x,curY:pt.y}); + },[tool,getSVGPoint]); - const onSVGMouseMove = useCallback((e) => { + const onSVGMove = useCallback(e=>{ if (!drawing) return; - const pt = getSVGPoint(e); - setDrawing(d=>({...d, curX:pt.x, curY:pt.y})); - },[drawing, getSVGPoint]); + const pt=getSVGPoint(e); + setDrawing(d=>({...d,curX:pt.x,curY:pt.y})); + },[drawing,getSVGPoint]); - const onSVGMouseUp = useCallback((e) => { + 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 newShape; - if (drawing.tool==='rect') { - newShape = { id:uid(), type:'rect', x:drawing.startX, y:drawing.startY, w:dx, h:dy, name:'', dimW:'', dimH:'' }; - } else if (drawing.tool==='line') { - newShape = { id:uid(), type:'line', x1:drawing.startX, y1:drawing.startY, x2:pt.x, y2:pt.y, name:'', dimLen:'' }; - } else if (drawing.tool==='circle') { - const r = Math.sqrt(dx*dx+dy*dy); - newShape = { id:uid(), type:'circle', cx:drawing.startX, cy:drawing.startY, r, name:'', dimR:'' }; - } - if (newShape) { - setShapes(p=>[...p, newShape]); - setSelected(newShape.id); - } + 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]); + },[drawing,getSVGPoint]); - const deleteSelected = () => { - setShapes(p=>p.filter(s=>s.id!==selected)); - setSelected(null); - }; + const deleteSelected = ()=>{setShapes(p=>p.filter(s=>s.id!==selected));setSelected(null);}; - const updateShape = (patch) => { - setShapes(p=>p.map(s=>s.id===selected?{...s,...patch}:s)); + // 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 unit = formula.toLowerCase().includes('fl') ? 'mm²' : 'mm'; - const entry = { - expr: formula, - result: result != null ? `${typeof result==='number'?result.toFixed(4).replace(/\.?0+$/,''):result} ${unit}` : '❌ Fehler', - ok: result != null, - }; - setResults(p=>[entry,...p].slice(0,20)); + 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)); }; - // Preview shape while drawing - let preview = null; - if (drawing) { - const dx = drawing.curX-drawing.startX, dy = drawing.curY-drawing.startY; - if (drawing.tool==='rect') { - preview = ; - } else if (drawing.tool==='line') { - preview = ; - } else if (drawing.tool==='circle') { - const r = Math.sqrt(dx*dx+dy*dy); - preview = ; - } + 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 ? 280 : 420; + const canvasH = mobile ? 260 : 420; return (
-
-

- Skizzen-Rechner -

+ {/* Header */} +
+

Skizzen-Rechner

- +
{/* Toolbar */}
- {TOOLS_LIST.map(t=>( + {TOOLS.map(t=>( ))} - {TOOLS_LIST.find(t2=>t2.id===tool)?.title} + + {TOOLS.find(t=>t.id===tool)?.title} +
- {/* Main layout */} + {/* Canvas + Panel */}
- {/* Canvas */} -
- + + + + + + + + + + + {snapGrid&&} + {shapes.map(s=>{if(tool==='select')setSelected(id);}}/>)} + {preview} + - {/* Grid */} - - - - - - - - - - {snapGrid && } - - {/* Shapes */} - {shapes.map(s=>( - { if(tool==='select') setSelected(id); }}/> - ))} - - {/* Preview */} - {preview} - -
- - {/* Right panel */} -
-
- +
+
+
- {/* Formula bar */} + {/* Rechner */}
-
RECHNER
-
- Beispiele: {Object.keys(vars).slice(0,4).join(' / ')||'(Formen zeichnen und benennen)'} -
-
- setFormula(e.target.value)} - onKeyDown={e=>e.key==='Enter'&&calcFormula()} - placeholder="z.B. Kasten_l / 2 oder Kasten_l * Kasten_b" - autoCapitalize="none" - style={{...S.inp,flex:1,fontFamily:"'Courier New',monospace",fontSize:13}}/> - - {results.length>0 && ( - - )} +
+
RECHNER
+ {results.length>0&&}
{/* Alle Variablen */} - {Object.keys(vars).length>0 && ( -
-
VARIABLEN
-
- {Object.entries(vars).map(([k,v])=>( -
- setFormula(f=>f+k)}>{k} - - = {typeof v==='number'?v.toFixed(2):v} - -
- ))} -
+ {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.map((r,i)=>( -
- - {r.expr} - - - {r.result} - -
- ))} - {results.length===0&&Object.keys(vars).length===0&&( -
- Zeichne Formen, benenne sie, dann kannst du hier damit rechnen. +
+ Formen zeichnen → benennen → hier rechnen
)} + {results.map(r=>( +
+ + {r.expr} + +
+ + {r.result||'❌ Fehler'} + + +
+
+ ))}
);