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 */}
+
{/* 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 */}
-
-
-
-
- {/* 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'}
+
+
+
+
+ ))}
);