Skizze: Proportionen, Input-Fix, Operator-Buttons, Mittelpunkt, Versatz, Verlauf löschen
This commit is contained in:
@@ -1,91 +1,100 @@
|
|||||||
import { useState, useRef, useCallback, useEffect, useId } from 'react';
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import { api, S } from '../lib.js';
|
import { api, S } from '../lib.js';
|
||||||
|
|
||||||
// ── Hilfsfunktionen ──────────────────────────────────────────────────────────
|
// ── Hilfsfunktionen ──────────────────────────────────────────────────────────
|
||||||
const uid = () => Math.random().toString(36).slice(2,8);
|
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) {
|
function parseDim(s) {
|
||||||
if (!s) return null;
|
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;
|
return isNaN(n) ? null : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate formula safely, variables = { name: value }
|
|
||||||
function evalFormula(expr, vars) {
|
function evalFormula(expr, vars) {
|
||||||
try {
|
try {
|
||||||
const sanitized = expr.replace(/[^0-9+\-*/().,%a-zA-Z_\s]/g,'');
|
const sanitized = expr.replace(/[^0-9+\-*/().,%a-zA-Z_\s]/g,'');
|
||||||
const keys = Object.keys(vars);
|
const keys = Object.keys(vars), vals = keys.map(k=>vars[k]);
|
||||||
const vals = keys.map(k => vars[k]);
|
|
||||||
// eslint-disable-next-line no-new-func
|
// eslint-disable-next-line no-new-func
|
||||||
return new Function(...keys, `"use strict"; return (${sanitized});`)(...vals);
|
return new Function(...keys, `"use strict"; return (${sanitized});`)(...vals);
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build variable map from shapes
|
|
||||||
function buildVars(shapes) {
|
function buildVars(shapes) {
|
||||||
const vars = {};
|
const vars = {};
|
||||||
shapes.forEach(s => {
|
shapes.forEach(s => {
|
||||||
const n = s.name?.replace(/[^a-zA-Z0-9_]/g,'_') || `_${s.id}`;
|
const n = (s.name||'').replace(/[^a-zA-Z0-9_]/g,'_');
|
||||||
|
if (!n) return;
|
||||||
if (s.type==='rect') {
|
if (s.type==='rect') {
|
||||||
const l = parseDim(s.dimW); const b = parseDim(s.dimH);
|
const l=parseDim(s.dimW), b=parseDim(s.dimH);
|
||||||
if (l!=null) { vars[`${n}_l`]=l; vars[`${n}_b`]=b??l; }
|
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); }
|
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==='line') { const v=parseDim(s.dimLen); if(v!=null) vars[`${n}_len`]=v; }
|
||||||
if (s.type==='circle') {
|
if (s.type==='circle') {
|
||||||
const r=parseDim(s.dimR);
|
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(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;
|
return vars;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── DimensionLine Component ───────────────────────────────────────────────────
|
// ── Row-Komponente (AUSSERHALB um Input-Bug zu vermeiden) ─────────────────────
|
||||||
|
function DimRow({ label, value, field, placeholder, onChange }) {
|
||||||
|
return (
|
||||||
|
<div style={{marginBottom:8}}>
|
||||||
|
<div style={{...S.head,marginBottom:3,fontSize:9}}>{label}</div>
|
||||||
|
<input value={value||''} onChange={e=>onChange({[field]:e.target.value})}
|
||||||
|
placeholder={placeholder} autoCapitalize="none"
|
||||||
|
style={{...S.inp,fontSize:12,padding:'6px 8px'}}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Maßlinie ─────────────────────────────────────────────────────────────────
|
||||||
function DimLine({x1,y1,x2,y2,label,color='#4ecdc4',offset=18}) {
|
function DimLine({x1,y1,x2,y2,label,color='#4ecdc4',offset=18}) {
|
||||||
const mx=(x1+x2)/2, my=(y1+y2)/2;
|
const mx=(x1+x2)/2, my=(y1+y2)/2;
|
||||||
const dx = x2-x1, dy = y2-y1;
|
const dx=x2-x1, dy=y2-y1, len=Math.sqrt(dx*dx+dy*dy);
|
||||||
const len = Math.sqrt(dx*dx+dy*dy);
|
|
||||||
if (len<4) return null;
|
if (len<4) return null;
|
||||||
const nx=-dy/len*offset, ny=dx/len*offset;
|
const nx=-dy/len*offset, ny=dx/len*offset;
|
||||||
const ax1=x1+nx, ay1=y1+ny, ax2=x2+nx, ay2=y2+ny;
|
|
||||||
return (
|
return (
|
||||||
<g>
|
<g>
|
||||||
<line x1={ax1} y1={ay1} x2={ax2} y2={ay2} stroke={color} strokeWidth={1} strokeDasharray="3,2" opacity={0.7}/>
|
<line x1={x1+nx} y1={y1+ny} x2={x2+nx} y2={y2+ny} stroke={color} strokeWidth={1} strokeDasharray="3,2" opacity={0.7}/>
|
||||||
<line x1={x1} y1={y1} x2={ax1} y2={ay1} stroke={color} strokeWidth={0.8} opacity={0.5}/>
|
<line x1={x1} y1={y1} x2={x1+nx} y2={y1+ny} stroke={color} strokeWidth={0.7} opacity={0.4}/>
|
||||||
<line x1={x2} y1={y2} x2={ax2} y2={ay2} stroke={color} strokeWidth={0.8} opacity={0.5}/>
|
<line x1={x2} y1={y2} x2={x2+nx} y2={y2+ny} stroke={color} strokeWidth={0.7} opacity={0.4}/>
|
||||||
<text x={mx+nx} y={my+ny} textAnchor="middle" dominantBaseline="middle"
|
<text x={mx+nx} y={my+ny} textAnchor="middle" dominantBaseline="middle"
|
||||||
fontSize={9} fill={color} fontFamily="'Courier New',monospace"
|
fontSize={9} fill={color} fontFamily="'Courier New',monospace" style={{userSelect:'none'}}
|
||||||
style={{userSelect:'none',paintOrder:'stroke'}}
|
stroke="#0a0a0c" strokeWidth={3} paintOrder="stroke">{label}</text>
|
||||||
stroke="#0d0d0f" strokeWidth={3}>{label}</text>
|
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Shape renderer ────────────────────────────────────────────────────────────
|
// ── Shape + Mittelpunkt + Versatz ─────────────────────────────────────────────
|
||||||
function Shape({ s, selected, onClick }) {
|
function Shape({ s, selected, onClick }) {
|
||||||
const sel = selected===s.id;
|
const sel = selected===s.id;
|
||||||
const c = sel ? '#4ecdc4' : (s.color||'#7ecaff');
|
const c = sel ? '#4ecdc4' : (s.color||'#7ecaff');
|
||||||
|
const offset = parseDim(s.offset) || 0; // Versatz in px (scaled)
|
||||||
|
|
||||||
if (s.type==='rect') {
|
if (s.type==='rect') {
|
||||||
const x=Math.min(s.x,s.x+s.w), y=Math.min(s.y,s.y+s.h);
|
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 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 (
|
return (
|
||||||
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
|
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
|
||||||
<rect x={x} y={y} width={w} height={h}
|
<rect x={x} y={y} width={w} height={h}
|
||||||
fill={sel?'rgba(78,205,196,0.08)':'rgba(126,202,255,0.06)'}
|
fill={sel?'rgba(78,205,196,0.06)':'rgba(126,202,255,0.04)'}
|
||||||
stroke={c} strokeWidth={sel?1.5:1}/>
|
stroke={c} strokeWidth={sel?1.5:1}/>
|
||||||
{s.name && <text x={x+w/2} y={y+h/2} textAnchor="middle" dominantBaseline="middle"
|
{/* Mittelpunkt */}
|
||||||
fontSize={11} fill={c} fontFamily="'Courier New',monospace" style={{userSelect:'none'}}>
|
<line x1={cx-6} y1={cy} x2={cx+6} y2={cy} stroke={c} strokeWidth={0.8} opacity={0.5}/>
|
||||||
{s.name}
|
<line x1={cx} y1={cy-6} x2={cx} y2={cy+6} stroke={c} strokeWidth={0.8} opacity={0.5}/>
|
||||||
</text>}
|
{/* Versatz */}
|
||||||
|
{offPx!==0&&<rect x={x-offPx} y={y-offPx} width={w+2*offPx} height={h+2*offPx}
|
||||||
|
fill="none" stroke={offPx>0?'#ffe66d':'#ff6b9d'} strokeWidth={1} strokeDasharray="4,3" opacity={0.7}/>}
|
||||||
|
{s.name&&<text x={cx} y={cy+2} textAnchor="middle" dominantBaseline="middle"
|
||||||
|
fontSize={10} fill={c} fontFamily="'Courier New',monospace" style={{userSelect:'none'}}
|
||||||
|
stroke="#0a0a0c" strokeWidth={3} paintOrder="stroke">{s.name}</text>}
|
||||||
{s.dimW&&<DimLine x1={x} y1={y+h} x2={x+w} y2={y+h} label={s.dimW} offset={14}/>}
|
{s.dimW&&<DimLine x1={x} y1={y+h} x2={x+w} y2={y+h} label={s.dimW} offset={14}/>}
|
||||||
{s.dimH&&<DimLine x1={x+w} y1={y} x2={x+w} y2={y+h} label={s.dimH} offset={14}/>}
|
{s.dimH&&<DimLine x1={x+w} y1={y} x2={x+w} y2={y+h} label={s.dimH} offset={14}/>}
|
||||||
{sel&&<>
|
{sel&&<>
|
||||||
@@ -100,36 +109,34 @@ function Shape({ s, selected, onClick }) {
|
|||||||
|
|
||||||
if (s.type==='line') {
|
if (s.type==='line') {
|
||||||
const len=dist(s.x1,s.y1,s.x2,s.y2);
|
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 (
|
return (
|
||||||
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
|
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
|
||||||
<line x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2}
|
<line x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke="transparent" strokeWidth={12}/>
|
||||||
stroke={c} strokeWidth={sel?2:1.5} strokeLinecap="round"/>
|
<line x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke={c} strokeWidth={sel?2:1.5} strokeLinecap="round"/>
|
||||||
<line x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2}
|
<circle cx={mx} cy={my} r={2} fill={c} opacity={0.6}/>
|
||||||
stroke="transparent" strokeWidth={12}/>
|
{(s.dimLen||s.name)&&len>20&&<DimLine x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} label={s.dimLen||s.name} offset={12}/>}
|
||||||
{(s.dimLen||s.name) && len>20 && (
|
{sel&&<><circle cx={s.x1} cy={s.y1} r={4} fill={c}/><circle cx={s.x2} cy={s.y2} r={4} fill={c}/></>}
|
||||||
<DimLine x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2}
|
|
||||||
label={s.dimLen||s.name} offset={12}/>
|
|
||||||
)}
|
|
||||||
{sel && <>
|
|
||||||
<circle cx={s.x1} cy={s.y1} r={4} fill={c}/>
|
|
||||||
<circle cx={s.x2} cy={s.y2} r={4} fill={c}/>
|
|
||||||
</>}
|
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s.type==='circle') {
|
if (s.type==='circle') {
|
||||||
|
const r=Math.max(1,s.r);
|
||||||
|
const offPx=s.offsetPx||0;
|
||||||
return (
|
return (
|
||||||
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
|
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
|
||||||
<circle cx={s.cx} cy={s.cy} r={Math.max(1,s.r)}
|
<circle cx={s.cx} cy={s.cy} r={r} fill={sel?'rgba(78,205,196,0.06)':'rgba(126,202,255,0.04)'} stroke={c} strokeWidth={sel?1.5:1}/>
|
||||||
fill={sel?'rgba(78,205,196,0.08)':'rgba(126,202,255,0.06)'}
|
{offPx!==0&&<circle cx={s.cx} cy={s.cy} r={r+offPx} fill="none" stroke={offPx>0?'#ffe66d':'#ff6b9d'} strokeWidth={1} strokeDasharray="4,3" opacity={0.7}/>}
|
||||||
stroke={c} strokeWidth={sel?1.5:1}/>
|
{/* Mittelpunkt */}
|
||||||
{s.name && <text x={s.cx} y={s.cy} textAnchor="middle" dominantBaseline="middle"
|
<line x1={s.cx-6} y1={s.cy} x2={s.cx+6} y2={s.cy} stroke={c} strokeWidth={0.8} opacity={0.5}/>
|
||||||
fontSize={10} fill={c} fontFamily="'Courier New',monospace" style={{userSelect:'none'}}>
|
<line x1={s.cx} y1={s.cy-6} x2={s.cx} y2={s.cy+6} stroke={c} strokeWidth={0.8} opacity={0.5}/>
|
||||||
{s.name}
|
{/* Radiuslinie */}
|
||||||
</text>}
|
<line x1={s.cx} y1={s.cy} x2={s.cx+r} y2={s.cy} stroke={c} strokeWidth={0.7} strokeDasharray="3,2" opacity={0.4}/>
|
||||||
{s.dimR && <DimLine x1={s.cx} y1={s.cy} x2={s.cx+s.r} y2={s.cy}
|
{s.name&&<text x={s.cx} y={s.cy+2} textAnchor="middle" dominantBaseline="middle"
|
||||||
label={'r='+s.dimR} offset={0} color="#ffe66d"/>}
|
fontSize={10} fill={c} fontFamily="'Courier New',monospace" style={{userSelect:'none'}}
|
||||||
|
stroke="#0a0a0c" strokeWidth={3} paintOrder="stroke">{s.name}</text>}
|
||||||
|
{s.dimR&&<DimLine x1={s.cx} y1={s.cy} x2={s.cx+r} y2={s.cy} label={'r='+s.dimR} offset={8} color="#ffe66d"/>}
|
||||||
{sel&&<circle cx={s.cx} cy={s.cy} r={4} fill={c}/>}
|
{sel&&<circle cx={s.cx} cy={s.cy} r={4} fill={c}/>}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
@@ -138,83 +145,85 @@ function Shape({ s, selected, onClick }) {
|
|||||||
if (s.type==='text') {
|
if (s.type==='text') {
|
||||||
return (
|
return (
|
||||||
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
|
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
|
||||||
<text x={s.x} y={s.y} fontSize={12} fill={sel?'#4ecdc4':'rgba(255,255,255,0.8)'}
|
<text x={s.x} y={s.y} fontSize={12} fill={sel?'#4ecdc4':'rgba(255,255,255,0.75)'}
|
||||||
fontFamily="'Courier New',monospace" style={{userSelect:'none'}}>
|
fontFamily="'Courier New',monospace" style={{userSelect:'none'}}>{s.label||'Text'}</text>
|
||||||
{s.label||'Text'}
|
|
||||||
</text>
|
|
||||||
{sel&&<rect x={s.x-2} y={s.y-14} width={Math.max(40,(s.label||'Text').length*7+4)} height={18}
|
{sel&&<rect x={s.x-2} y={s.y-14} width={Math.max(40,(s.label||'Text').length*7+4)} height={18}
|
||||||
fill="none" stroke="#4ecdc4" strokeWidth={1} strokeDasharray="3,2"/>}
|
fill="none" stroke="#4ecdc4" strokeWidth={1} strokeDasharray="3,2"/>}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Properties Panel ──────────────────────────────────────────────────────────
|
// ── Properties Panel ──────────────────────────────────────────────────────────
|
||||||
function PropsPanel({ shape, onChange, onDelete, vars }) {
|
function PropsPanel({ shape, onChange, onDelete, vars, scale, onScaleChange }) {
|
||||||
if (!shape) return (
|
if (!shape) return (
|
||||||
<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:11,padding:'14px 0',textAlign:'center'}}>
|
<div style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:10,padding:'14px 0',textAlign:'center',lineHeight:1.8}}>
|
||||||
Form auswählen oder zeichnen
|
Form auswählen oder zeichnen.<br/>
|
||||||
|
<span style={{fontSize:9,opacity:0.6}}>V=Auswählen R=Rect L=Linie C=Kreis</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Row = ({label, value, field, placeholder}) => (
|
const shapeVarKeys = Object.keys(vars).filter(k=>k.startsWith((shape.name||'').replace(/[^a-zA-Z0-9_]/g,'_')+'_'));
|
||||||
<div style={{marginBottom:8}}>
|
|
||||||
<div style={{...S.head,marginBottom:3,fontSize:9}}>{label}</div>
|
|
||||||
<input value={value||''} onChange={e=>onChange({[field]:e.target.value})}
|
|
||||||
placeholder={placeholder} autoCapitalize="none"
|
|
||||||
style={{...S.inp,fontSize:12,padding:'6px 8px'}}/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{...S.head,marginBottom:10}}>EIGENSCHAFTEN</div>
|
<div style={{...S.head,marginBottom:10}}>EIGENSCHAFTEN</div>
|
||||||
<Row label="NAME" value={shape.name} field="name" placeholder="z.B. Kasten"/>
|
<DimRow label="NAME" value={shape.name} field="name" placeholder="z.B. Kasten" onChange={onChange}/>
|
||||||
{shape.type==='rect'&&<>
|
{shape.type==='rect'&&<>
|
||||||
<Row label="LÄNGE (z.B. 225mm)" value={shape.dimW} field="dimW" placeholder="225mm"/>
|
<DimRow label="LÄNGE" value={shape.dimW} field="dimW" placeholder="225mm" onChange={onChange}/>
|
||||||
<Row label="BREITE (z.B. 30mm)" value={shape.dimH} field="dimH" placeholder="30mm"/>
|
<DimRow label="BREITE" value={shape.dimH} field="dimH" placeholder="30mm" onChange={onChange}/>
|
||||||
|
<DimRow label="VERSATZ (+ aussen / − innen)" value={shape.dimOffset} field="dimOffset" placeholder="5mm" onChange={onChange}/>
|
||||||
</>}
|
</>}
|
||||||
{shape.type==='line' && <Row label="LÄNGE" value={shape.dimLen} field="dimLen" placeholder="100mm"/>}
|
{shape.type==='line'&&<DimRow label="LÄNGE" value={shape.dimLen} field="dimLen" placeholder="100mm" onChange={onChange}/>}
|
||||||
{shape.type==='circle' && <Row label="RADIUS" value={shape.dimR} field="dimR" placeholder="50mm"/>}
|
{shape.type==='circle'&&<>
|
||||||
{shape.type==='text' && <Row label="TEXT" value={shape.label} field="label" placeholder="Beschriftung"/>}
|
<DimRow label="RADIUS" value={shape.dimR} field="dimR" placeholder="50mm" onChange={onChange}/>
|
||||||
|
<DimRow label="VERSATZ (+ aussen / − innen)" value={shape.dimOffset} field="dimOffset" placeholder="5mm" onChange={onChange}/>
|
||||||
|
</>}
|
||||||
|
{shape.type==='text'&&<DimRow label="TEXT" value={shape.label} field="label" placeholder="Beschriftung" onChange={onChange}/>}
|
||||||
|
|
||||||
<button onClick={onDelete}
|
<button onClick={onDelete}
|
||||||
style={{...S.btn('#ff6b9d',true),width:'100%',textAlign:'center',padding:'7px 0',fontSize:11,marginTop:4}}>
|
style={{...S.btn('#ff6b9d',true),width:'100%',textAlign:'center',padding:'7px 0',fontSize:11,marginTop:4}}>
|
||||||
✕ Löschen
|
✕ Löschen
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Variablen für diese Form */}
|
{shapeVarKeys.length>0&&(
|
||||||
{shape.name && (
|
<div style={{marginTop:12}}>
|
||||||
<div style={{marginTop:14}}>
|
<div style={{...S.head,marginBottom:5,fontSize:9}}>VARIABLEN</div>
|
||||||
<div style={{...S.head,marginBottom:6,fontSize:9}}>VERFÜGBARE VARIABLEN</div>
|
{shapeVarKeys.map(k=>(
|
||||||
{Object.entries(vars)
|
<div key={k} style={{display:'flex',justifyContent:'space-between',padding:'3px 0',
|
||||||
.filter(([k])=>k.startsWith((shape.name||'').replace(/[^a-zA-Z0-9_]/g,'_')+'_'))
|
borderBottom:'1px solid rgba(255,255,255,0.04)'}}>
|
||||||
.map(([k,v])=>(
|
|
||||||
<div key={k} style={{display:'flex',justifyContent:'space-between',
|
|
||||||
padding:'3px 0',borderBottom:'1px solid rgba(255,255,255,0.04)'}}>
|
|
||||||
<code style={{color:'#4ecdc4',fontSize:10}}>{k}</code>
|
<code style={{color:'#4ecdc4',fontSize:10}}>{k}</code>
|
||||||
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10}}>
|
<span style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10}}>
|
||||||
{typeof v==='number'?v.toFixed(2):v}
|
{typeof vars[k]==='number'?vars[k].toFixed(2):vars[k]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div style={{marginTop:14}}>
|
||||||
|
<div style={{...S.head,marginBottom:5,fontSize:9}}>MASSSTAB (mm/px)</div>
|
||||||
|
<div style={{display:'flex',gap:6,alignItems:'center'}}>
|
||||||
|
<input type="number" value={scale} onChange={e=>onScaleChange(parseFloat(e.target.value)||1)}
|
||||||
|
step={0.1} min={0.1} style={{...S.inp,flex:1,fontSize:12,padding:'5px 8px'}}/>
|
||||||
|
<span style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9}}>mm/px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Main Component ────────────────────────────────────────────────────────────
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
||||||
const TOOLS_LIST = [
|
const TOOLS = [
|
||||||
{id:'select',label:'▲',title:'Auswählen (V)'},
|
{id:'select',label:'▲',title:'Auswählen (V)'},
|
||||||
{id:'rect', label:'▭',title:'Rechteck (R)'},
|
{id:'rect', label:'▭',title:'Rechteck (R)'},
|
||||||
{id:'line', label:'╱',title:'Linie (L)'},
|
{id:'line', label:'╱',title:'Linie (L)'},
|
||||||
{id:'circle',label:'○',title:'Kreis (C)'},
|
{id:'circle',label:'○',title:'Kreis (C)'},
|
||||||
{id:'text', label:'T', title:'Text (T)'},
|
{id:'text', label:'T', title:'Text (T)'},
|
||||||
];
|
];
|
||||||
|
|
||||||
const GRID = 10;
|
const GRID = 10;
|
||||||
|
const CALC_BTNS = ['(',')',' ','7','8','9','×','4','5','6','÷','1','2','3','-','0','.','=','+'];
|
||||||
|
|
||||||
export default function Skizze({ toast, mobile }) {
|
export default function Skizze({ toast, mobile }) {
|
||||||
const [shapes, setShapes] = useState([]);
|
const [shapes, setShapes] = useState([]);
|
||||||
@@ -224,9 +233,7 @@ export default function Skizze({ toast, mobile }) {
|
|||||||
const [formula, setFormula] = useState('');
|
const [formula, setFormula] = useState('');
|
||||||
const [results, setResults] = useState([]);
|
const [results, setResults] = useState([]);
|
||||||
const [snapGrid, setSnapGrid] = useState(true);
|
const [snapGrid, setSnapGrid] = useState(true);
|
||||||
const [sketches, setSketches] = useState([]);
|
const [scale, setScale] = useState(1); // mm per pixel
|
||||||
const [sketchName, setSketchName] = useState('Skizze 1');
|
|
||||||
const [currentId, setCurrentId] = useState(null);
|
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const formulaRef= useRef(null);
|
const formulaRef= useRef(null);
|
||||||
|
|
||||||
@@ -237,244 +244,238 @@ export default function Skizze({ toast, mobile }) {
|
|||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
const h = e => {
|
const h = e => {
|
||||||
if (e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA') return;
|
if (e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA') return;
|
||||||
if (e.key==='v'||e.key==='V') setTool('select');
|
const map={v:'select',r:'rect',l:'line',c:'circle',t:'text'};
|
||||||
if (e.key==='r'||e.key==='R') setTool('rect');
|
if (map[e.key.toLowerCase()]) setTool(map[e.key.toLowerCase()]);
|
||||||
if (e.key==='l'||e.key==='L') setTool('line');
|
if ((e.key==='Delete'||e.key==='Backspace')&&selected) deleteSelected();
|
||||||
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(); }
|
|
||||||
if (e.key==='Escape') { setSelected(null); setDrawing(null); }
|
if (e.key==='Escape') { setSelected(null); setDrawing(null); }
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown',h);
|
window.addEventListener('keydown',h);
|
||||||
return()=>window.removeEventListener('keydown',h);
|
return()=>window.removeEventListener('keydown',h);
|
||||||
},[selected, shapes]);
|
},[selected]);
|
||||||
|
|
||||||
const getSVGPoint = useCallback((e) => {
|
const getSVGPoint = useCallback(e=>{
|
||||||
const rect=svgRef.current?.getBoundingClientRect();
|
const rect=svgRef.current?.getBoundingClientRect();
|
||||||
if (!rect) return{x:0,y:0};
|
if (!rect) return{x:0,y:0};
|
||||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
const cx=e.touches?e.touches[0].clientX:e.clientX;
|
||||||
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
const cy=e.touches?e.touches[0].clientY:e.clientY;
|
||||||
const x = clientX - rect.left;
|
const x=cx-rect.left, y=cy-rect.top;
|
||||||
const y = clientY - rect.top;
|
return {x:snapGrid?snapToGrid(x,GRID):x, y:snapGrid?snapToGrid(y,GRID):y};
|
||||||
return {
|
|
||||||
x: snapGrid ? snapToGrid(x, GRID) : x,
|
|
||||||
y: snapGrid ? snapToGrid(y, GRID) : y,
|
|
||||||
};
|
|
||||||
},[snapGrid]);
|
},[snapGrid]);
|
||||||
|
|
||||||
const onSVGMouseDown = useCallback((e) => {
|
const onSVGDown = useCallback(e=>{
|
||||||
if (tool==='select'){setSelected(null);return;}
|
if (tool==='select'){setSelected(null);return;}
|
||||||
const pt=getSVGPoint(e);
|
const pt=getSVGPoint(e);
|
||||||
if (tool==='text'){
|
if (tool==='text'){
|
||||||
setShapes(p=>[...p,{id:uid(),type:'text',x:pt.x,y:pt.y,label:'Text',name:''}]);
|
setShapes(p=>[...p,{id:uid(),type:'text',x:pt.x,y:pt.y,label:'Text',name:''}]);
|
||||||
setTool('select');
|
setTool('select'); return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
setDrawing({tool,startX:pt.x,startY:pt.y,curX:pt.x,curY:pt.y});
|
setDrawing({tool,startX:pt.x,startY:pt.y,curX:pt.x,curY:pt.y});
|
||||||
},[tool,getSVGPoint]);
|
},[tool,getSVGPoint]);
|
||||||
|
|
||||||
const onSVGMouseMove = useCallback((e) => {
|
const onSVGMove = useCallback(e=>{
|
||||||
if (!drawing) return;
|
if (!drawing) return;
|
||||||
const pt=getSVGPoint(e);
|
const pt=getSVGPoint(e);
|
||||||
setDrawing(d=>({...d,curX:pt.x,curY:pt.y}));
|
setDrawing(d=>({...d,curX:pt.x,curY:pt.y}));
|
||||||
},[drawing,getSVGPoint]);
|
},[drawing,getSVGPoint]);
|
||||||
|
|
||||||
const onSVGMouseUp = useCallback((e) => {
|
const onSVGUp = useCallback(e=>{
|
||||||
if (!drawing) return;
|
if (!drawing) return;
|
||||||
const pt=getSVGPoint(e);
|
const pt=getSVGPoint(e);
|
||||||
const dx=pt.x-drawing.startX, dy=pt.y-drawing.startY;
|
const dx=pt.x-drawing.startX, dy=pt.y-drawing.startY;
|
||||||
if (Math.abs(dx)<4&&Math.abs(dy)<4){setDrawing(null);return;}
|
if (Math.abs(dx)<4&&Math.abs(dy)<4){setDrawing(null);return;}
|
||||||
let newShape;
|
let s;
|
||||||
if (drawing.tool==='rect') {
|
if (drawing.tool==='rect') s={id:uid(),type:'rect',x:drawing.startX,y:drawing.startY,w:dx,h:dy,name:'',dimW:'',dimH:'',dimOffset:''};
|
||||||
newShape = { id:uid(), type:'rect', x:drawing.startX, y:drawing.startY, w:dx, h:dy, name:'', dimW:'', dimH:'' };
|
if (drawing.tool==='line') s={id:uid(),type:'line',x1:drawing.startX,y1:drawing.startY,x2:pt.x,y2:pt.y,name:'',dimLen:''};
|
||||||
} else if (drawing.tool==='line') {
|
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:''};
|
||||||
newShape = { id:uid(), type:'line', x1:drawing.startX, y1:drawing.startY, x2:pt.x, y2:pt.y, name:'', dimLen:'' };
|
if (s){setShapes(p=>[...p,s]);setSelected(s.id);}
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
setDrawing(null);
|
setDrawing(null);
|
||||||
},[drawing,getSVGPoint]);
|
},[drawing,getSVGPoint]);
|
||||||
|
|
||||||
const deleteSelected = () => {
|
const deleteSelected = ()=>{setShapes(p=>p.filter(s=>s.id!==selected));setSelected(null);};
|
||||||
setShapes(p=>p.filter(s=>s.id!==selected));
|
|
||||||
setSelected(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateShape = (patch) => {
|
// Update shape + auto-resize visual proportions based on scale
|
||||||
setShapes(p=>p.map(s=>s.id===selected?{...s,...patch}:s));
|
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 = () => {
|
const calcFormula = () => {
|
||||||
if (!formula.trim()) return;
|
if (!formula.trim()) return;
|
||||||
const result=evalFormula(formula,vars);
|
const result=evalFormula(formula,vars);
|
||||||
const unit = formula.toLowerCase().includes('fl') ? 'mm²' : 'mm';
|
const isArea=formula.toLowerCase().includes('_fl');
|
||||||
const entry = {
|
setResults(p=>[{
|
||||||
expr: formula,
|
id:uid(), expr:formula,
|
||||||
result: result != null ? `${typeof result==='number'?result.toFixed(4).replace(/\.?0+$/,''):result} ${unit}` : '❌ Fehler',
|
result:result!=null?`${typeof result==='number'?result.toFixed(4).replace(/.?0+$/,''):result}${isArea?' mm²':' mm'}`:null,
|
||||||
ok: result != null,
|
},...p].slice(0,30));
|
||||||
};
|
|
||||||
setResults(p=>[entry,...p].slice(0,20));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Preview shape while drawing
|
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;
|
let preview=null;
|
||||||
if (drawing){
|
if (drawing){
|
||||||
const dx=drawing.curX-drawing.startX, dy=drawing.curY-drawing.startY;
|
const dx=drawing.curX-drawing.startX, dy=drawing.curY-drawing.startY;
|
||||||
if (drawing.tool==='rect') {
|
const pc='#4ecdc4';
|
||||||
preview = <rect x={Math.min(drawing.startX,drawing.curX)} y={Math.min(drawing.startY,drawing.curY)}
|
if (drawing.tool==='rect') preview=<rect x={Math.min(drawing.startX,drawing.curX)} y={Math.min(drawing.startY,drawing.curY)} width={Math.abs(dx)} height={Math.abs(dy)} fill="rgba(78,205,196,0.04)" stroke={pc} strokeWidth={1} strokeDasharray="4,3"/>;
|
||||||
width={Math.abs(dx)} height={Math.abs(dy)}
|
if (drawing.tool==='line') preview=<line x1={drawing.startX} y1={drawing.startY} x2={drawing.curX} y2={drawing.curY} stroke={pc} strokeWidth={1.5} strokeDasharray="4,3"/>;
|
||||||
fill="rgba(78,205,196,0.05)" stroke="#4ecdc4" strokeWidth={1} strokeDasharray="4,3"/>;
|
if (drawing.tool==='circle'){const r=dist(drawing.startX,drawing.startY,drawing.curX,drawing.curY); preview=<circle cx={drawing.startX} cy={drawing.startY} r={r} fill="rgba(78,205,196,0.04)" stroke={pc} strokeWidth={1} strokeDasharray="4,3"/>;}
|
||||||
} else if (drawing.tool==='line') {
|
|
||||||
preview = <line x1={drawing.startX} y1={drawing.startY} x2={drawing.curX} y2={drawing.curY}
|
|
||||||
stroke="#4ecdc4" strokeWidth={1.5} strokeDasharray="4,3"/>;
|
|
||||||
} else if (drawing.tool==='circle') {
|
|
||||||
const r = Math.sqrt(dx*dx+dy*dy);
|
|
||||||
preview = <circle cx={drawing.startX} cy={drawing.startY} r={r}
|
|
||||||
fill="rgba(78,205,196,0.05)" stroke="#4ecdc4" strokeWidth={1} strokeDasharray="4,3"/>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvasH = mobile ? 280 : 420;
|
const canvasH = mobile ? 260 : 420;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:1100}}>
|
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:1100}}>
|
||||||
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:14}}>
|
{/* Header */}
|
||||||
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?17:22,margin:0}}>
|
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:12,flexWrap:'wrap',gap:8}}>
|
||||||
Skizzen-Rechner
|
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?16:22,margin:0}}>Skizzen-Rechner</h1>
|
||||||
</h1>
|
|
||||||
<div style={{display:'flex',gap:8,alignItems:'center'}}>
|
<div style={{display:'flex',gap:8,alignItems:'center'}}>
|
||||||
<label style={{display:'flex',alignItems:'center',gap:5,cursor:'pointer'}}>
|
<label style={{display:'flex',alignItems:'center',gap:5,cursor:'pointer'}}>
|
||||||
<input type="checkbox" checked={snapGrid} onChange={e=>setSnapGrid(e.target.checked)}
|
<input type="checkbox" checked={snapGrid} onChange={e=>setSnapGrid(e.target.checked)} style={{accentColor:'#4ecdc4'}}/>
|
||||||
style={{accentColor:'#4ecdc4'}}/>
|
<span style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>Raster</span>
|
||||||
<span style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10}}>Raster</span>
|
|
||||||
</label>
|
</label>
|
||||||
<button onClick={()=>setShapes([])} title="Alles löschen"
|
<button onClick={()=>{setShapes([]);setSelected(null);}}
|
||||||
style={{...S.btn('#ff6b9d',true),fontSize:11,padding:'5px 10px'}}>✕ Leeren</button>
|
style={{...S.btn('#ff6b9d',true),fontSize:10,padding:'5px 10px'}}>✕ Leeren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div style={{display:'flex',gap:5,marginBottom:10,flexWrap:'wrap'}}>
|
<div style={{display:'flex',gap:5,marginBottom:10,flexWrap:'wrap'}}>
|
||||||
{TOOLS_LIST.map(t=>(
|
{TOOLS.map(t=>(
|
||||||
<button key={t.id} onClick={()=>setTool(t.id)} title={t.title} style={{
|
<button key={t.id} onClick={()=>setTool(t.id)} title={t.title} style={{
|
||||||
padding:'7px 14px', borderRadius:8, fontFamily:'monospace',
|
padding:'7px 13px',borderRadius:8,fontFamily:'monospace',fontSize:t.id==='text'?14:16,
|
||||||
fontSize: t.id==='text'?14:16, cursor:'pointer', fontWeight:700,
|
cursor:'pointer',fontWeight:700,
|
||||||
background:tool===t.id?'#4ecdc4':'rgba(255,255,255,0.06)',
|
background:tool===t.id?'#4ecdc4':'rgba(255,255,255,0.06)',
|
||||||
color: tool===t.id?'#0d0d0f':'rgba(255,255,255,0.6)',
|
color: tool===t.id?'#0d0d0f':'rgba(255,255,255,0.6)',
|
||||||
border: tool===t.id?'none':'1px solid rgba(255,255,255,0.1)',
|
border: tool===t.id?'none':'1px solid rgba(255,255,255,0.1)',
|
||||||
}}>{t.label}</button>
|
}}>{t.label}</button>
|
||||||
))}
|
))}
|
||||||
<span style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:9,
|
<span style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:9,alignSelf:'center',marginLeft:4}}>
|
||||||
alignSelf:'center',marginLeft:4}}>{TOOLS_LIST.find(t2=>t2.id===tool)?.title}</span>
|
{TOOLS.find(t=>t.id===tool)?.title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main layout */}
|
{/* Canvas + Panel */}
|
||||||
<div style={{display:'flex',gap:12,flexDirection:mobile?'column':'row'}}>
|
<div style={{display:'flex',gap:12,flexDirection:mobile?'column':'row'}}>
|
||||||
{/* Canvas */}
|
<svg ref={svgRef} style={{flex:1,minWidth:0,background:'#0a0a0c',border:'1px solid rgba(255,255,255,0.08)',borderRadius:10,display:'block',cursor:tool==='select'?'default':tool==='text'?'text':'crosshair'}}
|
||||||
<div style={{flex:1,minWidth:0}}>
|
height={canvasH}
|
||||||
<svg ref={svgRef} width="100%" height={canvasH}
|
onMouseDown={onSVGDown} onMouseMove={onSVGMove} onMouseUp={onSVGUp}
|
||||||
style={{background:'#0a0a0c',border:'1px solid rgba(255,255,255,0.08)',
|
onTouchStart={onSVGDown} onTouchMove={onSVGMove} onTouchEnd={onSVGUp}>
|
||||||
borderRadius:10,display:'block',
|
|
||||||
cursor: tool==='select'?'default':tool==='text'?'text':'crosshair'}}
|
|
||||||
onMouseDown={onSVGMouseDown} onMouseMove={onSVGMouseMove} onMouseUp={onSVGMouseUp}
|
|
||||||
onTouchStart={onSVGMouseDown} onTouchMove={onSVGMouseMove} onTouchEnd={onSVGMouseUp}>
|
|
||||||
|
|
||||||
{/* Grid */}
|
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="grid10" width={GRID} height={GRID} patternUnits="userSpaceOnUse">
|
<pattern id="g10" width={GRID} height={GRID} patternUnits="userSpaceOnUse">
|
||||||
<path d={`M ${GRID} 0 L 0 0 0 ${GRID}`} fill="none" stroke="rgba(255,255,255,0.04)" strokeWidth={0.5}/>
|
<path d={`M ${GRID} 0 L 0 0 0 ${GRID}`} fill="none" stroke="rgba(255,255,255,0.04)" strokeWidth={0.5}/>
|
||||||
</pattern>
|
</pattern>
|
||||||
<pattern id="grid50" width={50} height={50} patternUnits="userSpaceOnUse">
|
<pattern id="g50" width={50} height={50} patternUnits="userSpaceOnUse">
|
||||||
<rect width={50} height={50} fill="url(#grid10)"/>
|
<rect width={50} height={50} fill="url(#g10)"/>
|
||||||
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(255,255,255,0.09)" strokeWidth={0.5}/>
|
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(255,255,255,0.08)" strokeWidth={0.5}/>
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
{snapGrid && <rect width="100%" height="100%" fill="url(#grid50)"/>}
|
{snapGrid&&<rect width="100%" height="100%" fill="url(#g50)"/>}
|
||||||
|
{shapes.map(s=><Shape key={s.id} s={s} selected={selected} onClick={id=>{if(tool==='select')setSelected(id);}}/>)}
|
||||||
{/* Shapes */}
|
|
||||||
{shapes.map(s=>(
|
|
||||||
<Shape key={s.id} s={s} selected={selected}
|
|
||||||
onClick={id=>{ if(tool==='select') setSelected(id); }}/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Preview */}
|
|
||||||
{preview}
|
{preview}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right panel */}
|
<div style={{width:mobile?'100%':210,flexShrink:0}}>
|
||||||
<div style={{width:mobile?'100%':220,flexShrink:0}}>
|
<div style={{...S.card}}>
|
||||||
<div style={{...S.card,marginBottom:10}}>
|
<PropsPanel shape={selShape} onChange={updateShape} onDelete={deleteSelected}
|
||||||
<PropsPanel shape={selShape} onChange={updateShape}
|
vars={vars} scale={scale} onScaleChange={setScale}/>
|
||||||
onDelete={deleteSelected} vars={vars}/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Formula bar */}
|
{/* Rechner */}
|
||||||
<div style={{...S.card,marginTop:12}}>
|
<div style={{...S.card,marginTop:12}}>
|
||||||
<div style={{...S.head,marginBottom:8}}>RECHNER</div>
|
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
|
||||||
<div style={{marginBottom:10,color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9,lineHeight:1.8}}>
|
<div style={{...S.head,marginBottom:0}}>RECHNER</div>
|
||||||
Beispiele: {Object.keys(vars).slice(0,4).join(' / ')||'(Formen zeichnen und benennen)'}
|
{results.length>0&&<button onClick={()=>setResults([])} style={{...S.btn('#ff6b9d',true),fontSize:10,padding:'3px 8px'}}>✕ Verlauf</button>}
|
||||||
</div>
|
|
||||||
<div style={{display:'flex',gap:8,marginBottom:10}}>
|
|
||||||
<input ref={formulaRef} value={formula} onChange={e=>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}}/>
|
|
||||||
<button onClick={calcFormula} style={{...S.btn('#4ecdc4'),padding:'0 16px',flexShrink:0}}>
|
|
||||||
= Berechnen
|
|
||||||
</button>
|
|
||||||
{results.length>0 && (
|
|
||||||
<button onClick={()=>setResults([])} style={{...S.btn('#ff6b9d',true),padding:'0 10px',fontSize:11}}>✕</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Alle Variablen */}
|
{/* Alle Variablen */}
|
||||||
{Object.keys(vars).length>0&&(
|
{Object.keys(vars).length>0&&(
|
||||||
<div style={{marginBottom:10,padding:'8px 10px',background:'rgba(0,0,0,0.3)',
|
<div style={{display:'flex',flexWrap:'wrap',gap:'3px 14px',marginBottom:10,padding:'7px 10px',
|
||||||
borderRadius:8,border:'1px solid rgba(255,255,255,0.05)'}}>
|
background:'rgba(0,0,0,0.3)',borderRadius:7,border:'1px solid rgba(255,255,255,0.05)'}}>
|
||||||
<div style={{...S.head,marginBottom:6,fontSize:9}}>VARIABLEN</div>
|
|
||||||
<div style={{display:'flex',flexWrap:'wrap',gap:'4px 16px'}}>
|
|
||||||
{Object.entries(vars).map(([k,v])=>(
|
{Object.entries(vars).map(([k,v])=>(
|
||||||
<div key={k} style={{display:'flex',gap:6,alignItems:'center'}}>
|
<span key={k} style={{cursor:'pointer'}} onClick={()=>{setFormula(f=>f+k);formulaRef.current?.focus();}}>
|
||||||
<code style={{color:'#4ecdc4',fontSize:10,cursor:'pointer'}}
|
<code style={{color:'#4ecdc4',fontSize:10}}>{k}</code>
|
||||||
onClick={()=>setFormula(f=>f+k)}>{k}</code>
|
<span style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10}}>={typeof v==='number'?v.toFixed(2):v}</span>
|
||||||
<span style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>
|
|
||||||
= {typeof v==='number'?v.toFixed(2):v}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div style={{display:'flex',gap:8,marginBottom:8}}>
|
||||||
|
<input ref={formulaRef} value={formula} onChange={e=>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}}/>
|
||||||
|
<button onClick={calcFormula} style={{...S.btn('#4ecdc4'),padding:'0 16px',flexShrink:0}}>= Berechnen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Operator-Buttons */}
|
||||||
|
<div style={{display:'grid',gridTemplateColumns:'repeat(5,1fr)',gap:5,marginBottom:10}}>
|
||||||
|
{['(',')',' × ',' ÷ ',' '].map((b,i)=>(
|
||||||
|
b.trim()===''?<div key={i}/>:
|
||||||
|
<button key={i} onClick={()=>insertCalc(b.trim())} style={{
|
||||||
|
background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)',
|
||||||
|
borderRadius:7,color:'rgba(255,255,255,0.7)',cursor:'pointer',
|
||||||
|
fontFamily:'monospace',fontSize:14,padding:'6px 0',fontWeight:700,
|
||||||
|
}}>{b.trim()==='×'?'×':b.trim()==='÷'?'÷':b.trim()}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Ergebnisse */}
|
{/* Ergebnisse */}
|
||||||
{results.map((r,i)=>(
|
|
||||||
<div key={i} style={{display:'flex',justifyContent:'space-between',alignItems:'center',
|
|
||||||
padding:'7px 10px',marginBottom:4,borderRadius:7,
|
|
||||||
background: r.ok?'rgba(78,205,196,0.06)':'rgba(255,107,157,0.06)',
|
|
||||||
border:`1px solid ${r.ok?'rgba(78,205,196,0.15)':'rgba(255,107,157,0.15)'}`}}>
|
|
||||||
<code style={{color:'rgba(255,255,255,0.5)',fontSize:12,fontFamily:"'Courier New',monospace"}}>
|
|
||||||
{r.expr}
|
|
||||||
</code>
|
|
||||||
<code style={{color:r.ok?'#4ecdc4':'#ff6b9d',fontSize:14,fontFamily:"'Courier New',monospace",fontWeight:700}}>
|
|
||||||
{r.result}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{results.length===0&&Object.keys(vars).length===0&&(
|
{results.length===0&&Object.keys(vars).length===0&&(
|
||||||
<div style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:11,textAlign:'center',padding:'8px 0'}}>
|
<div style={{color:'rgba(255,255,255,0.15)',fontFamily:'monospace',fontSize:11,textAlign:'center',padding:'8px 0'}}>
|
||||||
Zeichne Formen, benenne sie, dann kannst du hier damit rechnen.
|
Formen zeichnen → benennen → hier rechnen
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{results.map(r=>(
|
||||||
|
<div key={r.id} style={{display:'flex',justifyContent:'space-between',alignItems:'center',
|
||||||
|
padding:'7px 10px',marginBottom:5,borderRadius:7,
|
||||||
|
background:r.result?'rgba(78,205,196,0.06)':'rgba(255,107,157,0.06)',
|
||||||
|
border:`1px solid ${r.result?'rgba(78,205,196,0.14)':'rgba(255,107,157,0.14)'}`}}>
|
||||||
|
<code style={{color:'rgba(255,255,255,0.45)',fontSize:11,fontFamily:"'Courier New',monospace",
|
||||||
|
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',maxWidth:'60%'}}>
|
||||||
|
{r.expr}
|
||||||
|
</code>
|
||||||
|
<div style={{display:'flex',alignItems:'center',gap:8}}>
|
||||||
|
<code style={{color:r.result?'#4ecdc4':'#ff6b9d',fontSize:14,fontFamily:"'Courier New',monospace",fontWeight:700}}>
|
||||||
|
{r.result||'❌ Fehler'}
|
||||||
|
</code>
|
||||||
|
<button onClick={()=>setResults(p=>p.filter(x=>x.id!==r.id))}
|
||||||
|
style={{background:'transparent',border:'none',cursor:'pointer',
|
||||||
|
color:'rgba(255,255,255,0.2)',fontSize:12,padding:'0 2px',lineHeight:1}}>✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user