feat(rc3): Crumbblocks UI Mission & Smart Routing 🎨

This commit is contained in:
Branko May Trinkwald
2025-12-23 20:52:00 +01:00
parent e52684d6f8
commit 01a01f53b4
27 changed files with 7551 additions and 25 deletions

View File

@@ -253,14 +253,20 @@ bash missions/robots/mond_maschine.sh
---
### 🎨 Crumbblocks & UI ⭐ NEU!
- **Dein Zeichen im Wald** - Designe ein UI-Element im Browser
- **Bridge-Technology** - Sende Daten via Clipboard vom Browser ins Terminal
- **Smart Evaluation** - Die Crew erkennt automatisch, was du gebaut hast!
## 🏷️ Version
**v0.1-robots-complete**
**v0.0-RC3-crumbblocks**
- Crumbblocks Integration (Browser <-> Terminal)
- Neue UI Mission "Dein Zeichen im Wald"
- Smart Routing für Evaluations-Skripte
- macOS Pfad-Fixes für Waldwächter
- 3 Robot-Missionen
- 17 Waldwächter komplett
- Logs im Repo
- Token-Tracking
- Crew Memory (log-basiert)
---

View File

@@ -565,32 +565,63 @@ function eule_memory() {
}
function eule_tokens() {
LOGDIR="\$HOME/.eule_logs"
if [[ -f "\$LOGDIR/token_log.json" ]]; then
# Use the new crew_tokens function from waldwaechter.sh
# All logs are now in the repo logs/ directory
if command -v crew_tokens &> /dev/null; then
crew_tokens
else
# Fallback if waldwaechter.sh not loaded
LOGDIR="${REPO_ROOT}/logs"
echo -e "\${CYAN}📊 Token-Verbrauch:\${NC}"
echo ""
TOTAL=0
# Filter leere Arrays und parse JSON robuster
while IFS= read -r line; do
# Skip leere Zeilen und [] Arrays
if [[ -z "\$line" ]] || [[ "\$line" == "[]" ]]; then
continue
fi
zeit=\$(echo "\$line" | jq -r '.zeit // "unknown"' 2>/dev/null)
# Unterstütze usage als String oder Objekt
tokens=\$(echo "\$line" | jq -r 'if .usage | type == "string" then .usage | fromjson | .total_tokens // 0 else .usage.total_tokens // 0 end' 2>/dev/null)
if [[ -d "\$LOGDIR" ]]; then
local total_tokens=0
local found_any=false
if [[ "\$tokens" =~ ^[0-9]+\$ ]]; then
TOTAL=\$((TOTAL + tokens))
echo " \$zeit: \$tokens Tokens"
for token_file in "\$LOGDIR"/*/token_log.json; do
if [[ -f "\$token_file" ]]; then
local character=\$(basename "\$(dirname "\$token_file")")
local char_tokens=\$(grep -v '^\[\]\$' "\$token_file" | jq -s 'map(.usage.total_tokens // 0) | add // 0' 2>/dev/null || echo 0)
if [[ \$char_tokens -gt 0 ]]; then
echo " \$character: \$char_tokens Tokens"
total_tokens=\$((total_tokens + char_tokens))
found_any=true
fi
fi
done
echo ""
if [[ \$found_any == true ]]; then
echo -e "\${GREEN}Gesamt: \$total_tokens Tokens\${NC}"
else
echo "Noch keine Token-Logs vorhanden"
fi
done < "\$LOGDIR/token_log.json"
else
echo "Log-Verzeichnis nicht gefunden: \$LOGDIR"
fi
echo ""
echo -e "\${GREEN}Gesamt: \$TOTAL Tokens\${NC}"
echo -e "\${YELLOW}Jede Frage ist wertvoll 🌲\${NC}"
else
echo "Noch keine Token-Logs."
echo -e "\${YELLOW}Token Budget & Philosophie:\${NC}"
echo " \"Was kostet die Frage eines Kindes?\""
echo " Im Wald unbezahlbar - Token lehren achtsames Fragen."
echo ""
if [[ -n "\${DAILY_TOKEN_BUDGET}" ]] && [[ "\${DAILY_TOKEN_BUDGET}" != "0" ]]; then
echo " Tages-Budget: \${DAILY_TOKEN_BUDGET} Tokens"
else
echo " Tages-Budget: Unbegrenzt"
fi
if [[ "\${ENABLE_TOKEN_TRACKING}" == "true" ]]; then
echo " Token-Tracking: Aktiviert"
fi
echo ""
echo -e "\${CYAN}Waldwächter (AI Charaktere):\${NC}"
echo ""
echo -e "\${YELLOW}Token-Logs:\${NC}"
if [[ \$found_any == false ]]; then
echo " Noch keine Logs vorhanden"
fi
fi
}

628
crumbblocks/bezier.html Normal file
View File

@@ -0,0 +1,628 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>OneLiner Painter • Bezier Pen + Editor</title>
<style>
:root{ --bg:#0b0b10; --panel:#12121a; --muted:#9aa3b2; --accent:#62d3a4; --accent2:#ffd166; --danger:#ff5c5c; }
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;display:grid;grid-template-rows:auto 1fr auto;background:var(--bg);color:#e7eaf0;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
header,footer{padding:12px 16px;background:var(--panel);border-bottom:1px solid #1e2230}
footer{border-top:1px solid #1e2230;border-bottom:none;font-size:.9rem;color:var(--muted)}
h1{font-size:1.05rem;margin:0}
.wrap{display:grid;grid-template-columns:380px 1fr;gap:16px;padding:16px}
@media (max-width:1100px){.wrap{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid #1e2230;border-radius:12px;padding:14px;display:grid;gap:12px}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.col{display:grid;gap:10px}
label{font-size:.9rem;color:#cfd5e3}
input[type="text"],textarea,select{width:100%;padding:10px;border-radius:10px;border:1px solid #2a3042;background:#0f121a;color:#e7eaf0}
input[type="color"]{width:48px;height:36px;border:none;background:none}
.btn{appearance:none;border:none;border-radius:10px;padding:10px 12px;background:#1f2636;color:#e7eaf0;cursor:pointer}
.btn:hover{background:#28314a}
.btn.accent{background:var(--accent);color:#0b1114;font-weight:600}
.btn.warn{background:var(--danger);color:#0b0b10}
.chip{display:inline-flex;align-items:center;gap:8px;background:#0f121a;border:1px dashed #2a3042;border-radius:10px;padding:6px 10px}
.kbd{padding:1px 6px;border-radius:8px;background:#1e2333;color:#cfd5e3;font-family:ui-monospace,Menlo,Monaco,monospace}
ul#strokeList{list-style:none;margin:0;padding:0;display:grid;gap:8px;max-height:240px;overflow:auto}
li.strokeItem{display:flex;align-items:center;gap:8px;background:#0f121a;border:1px solid #2a3042;border-radius:10px;padding:8px}
.canvasWrap{position:relative;background:#0f121a;border:1px solid #1e2230;border-radius:12px;overflow:hidden}
svg{display:block;width:100%;height:100%;background:#0f121a;touch-action:none}
.badge{font-size:.75rem;color:#0b1114;background:var(--accent);border-radius:999px;padding:2px 8px}
.muted{color:var(--muted)}
.sline{fill:none;stroke-linecap:round;stroke-linejoin:round}
.markerStart{fill:#35c759;stroke:none}
.markerStop{fill:#ff3b30;stroke:none}
.anchor{fill:#31e07b;stroke:#0b0b10;stroke-width:2;cursor:grab; pointer-events:all}
.handle{fill:#ffe08a;stroke:#0b0b10;stroke-width:2;cursor:grab; pointer-events:all}
.handleLine{stroke:#ffe08a;stroke-width:2.5;stroke-dasharray:4 4; pointer-events:none}
.ghost{fill:none;stroke:#ffffff55;stroke-width:2;stroke-dasharray:6 8}
#bezierUI{ pointer-events:none }
.selected{filter:drop-shadow(0 0 4px #ffd166)}
.hint{font-size:.85rem;color:#cfd5e3;line-height:1.3}
</style>
</head>
<body>
<header>
<h1>OneLiner Painter • <span class="badge">Bezier-Pen</span> + Editor • Replay • SVG/PNG/JSON</h1>
</header>
<div class="wrap">
<aside class="panel">
<h2 style="margin:0;font-size:1rem">Werkzeug</h2>
<div class="row">
<label class="chip"><input type="checkbox" id="sparkMode"> <span>Funken (Shift)</span></label>
<label class="chip"><input type="checkbox" id="bitMode"> <span>Bits „1“</span></label>
<label class="chip"><input type="checkbox" id="markers"> <span>Start/Stop</span></label>
<label class="chip"><input type="checkbox" id="frameOn" checked> <span>Crumblines-Frame</span></label>
</div>
<div class="row">
<label>Strichstärke <input type="range" id="stroke" min="4" max="28" step="1" value="12"></label>
<label>Sampler
<select id="sampler">
<option value="raw">Raw Polyline</option>
<option value="quad">Quadratic</option>
<option value="cubic" selected>CatmullRom → Cubic</option>
</select>
</label>
</div>
<div class="row">
<label>Farbe A <input type="color" id="colorA" value="#62d3a4"></label>
<label>Farbe B <input type="color" id="colorB" value="#ffd166"></label>
<label>Funken <input type="color" id="colorSpark" value="#e7eaf0"></label>
</div>
<div class="row">
<label class="chip"><input type="radio" name="mode" value="A" checked> <span>Stroke A</span></label>
<label class="chip"><input type="radio" name="mode" value="B"> <span>Stroke B</span></label>
<label class="chip"><input type="radio" name="mode" value="GRAD"> <span>Verlauf A→B</span></label>
</div>
<div class="col" style="margin-top:6px">
<strong>BezierPen (Anker setzen)</strong>
<div class="row">
<label class="chip"><input type="checkbox" id="penMode"> <span>Pen aktiv</span></label>
<button class="btn" id="finishPen" disabled>Fertig (↵)</button>
<span class="muted">Shift: Winkel snap • Alt: Ecke • ⌫: Letzten Anchor löschen</span>
</div>
</div>
<div class="col" style="margin-top:6px">
<strong>BezierEditor</strong>
<div class="row">
<label class="chip"><input type="checkbox" id="editBezier"> <span>Bearbeiten</span></label>
<label class="chip"><input type="checkbox" id="linkHandles" checked> <span>SpiegelGriffe</span></label>
<button class="btn" id="toBezier">Auswahl → Bezier</button>
<button class="btn" id="splitHandles">Anker: Split</button>
<button class="btn warn" id="delAnchor" disabled>Anker löschen</button>
</div>
<p class="hint">Klicke einen Stroke → <em>Bearbeiten</em>. Doppelklick auf Pfad fügt einen Anker. <span class="kbd">L</span> toggelt SpiegelGriffe.</p>
</div>
<div>
<strong>Strokes</strong>
<ul id="strokeList" aria-label="Strich-Liste"></ul>
</div>
<div class="row">
<button class="btn" id="undo">Letzten Stroke löschen</button>
<button class="btn warn" id="clear">Alles löschen</button>
</div>
<div class="row">
<button class="btn" id="replay">▶︎ Replay</button>
<button class="btn" id="copySVG">SVG kopieren</button>
<button class="btn" id="downloadSVG">SVG speichern</button>
<button class="btn" id="downloadPNG">PNG speichern</button>
<button class="btn accent" id="copyJSON">MotionJSON kopieren</button>
<button class="btn" id="downloadJSON">JSON speichern</button>
</div>
</aside>
<main class="canvasWrap" aria-label="Zeichenfläche">
<svg id="stage" viewBox="0 0 1200 800" role="img" aria-labelledby="svgTitle svgDesc">
<title id="svgTitle">Omi Omega OneLiner</title>
<desc id="svgDesc">a→Spirale→y; Bits 1+1; Funken.</desc>
<defs>
<linearGradient id="gradA2B" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#62d3a4" />
<stop offset="50%" stop-color="#62d3a4" />
<stop offset="51%" stop-color="#ffd166" />
<stop offset="100%" stop-color="#ffd166" />
</linearGradient>
</defs>
<rect id="frameRect" x="16.5" y="16.5" width="1167" height="767" fill="none" stroke="rgba(255,255,255,.2)" stroke-width="1.5" stroke-dasharray="6 10"/>
<g id="art">
<g id="strokes"></g>
<g id="bits"></g>
<g id="sparks" stroke="#e7eaf0" stroke-width="8" stroke-linecap="round"></g>
<g id="markersG"></g>
<path id="ghost" class="ghost" d=""/>
<g id="bezierUI"></g>
</g>
</svg>
</main>
</div>
<footer>
<span>Neu: richtiger BezierPen wie in Illustrator/Inkscape (Anker setzen & ziehen).</span>
</footer>
<script>
(function(){
const stage = document.getElementById('stage');
const strokesG= document.getElementById('strokes');
const bitsG = document.getElementById('bits');
const sparksG = document.getElementById('sparks');
const markersG= document.getElementById('markersG');
const ghost = document.getElementById('ghost');
const bezierUI= document.getElementById('bezierUI');
const frameRect = document.getElementById('frameRect');
const strokeList = document.getElementById('strokeList');
// Controls
const sparkMode = document.getElementById('sparkMode');
const bitMode = document.getElementById('bitMode');
const markersCb = document.getElementById('markers');
const frameOnCb = document.getElementById('frameOn');
const strokeInp = document.getElementById('stroke');
const samplerSel= document.getElementById('sampler');
const colorAInp = document.getElementById('colorA');
const colorBInp = document.getElementById('colorB');
const colorSInp = document.getElementById('colorSpark');
const penModeCb = document.getElementById('penMode');
const finishPenBtn = document.getElementById('finishPen');
const editBezierCb = document.getElementById('editBezier');
const linkHandlesCb= document.getElementById('linkHandles');
const toBezierBtn = document.getElementById('toBezier');
const splitBtn = document.getElementById('splitHandles');
const delAnchorBtn = document.getElementById('delAnchor');
const undoBtn = document.getElementById('undo');
const clearBtn = document.getElementById('clear');
const copySVGBtn = document.getElementById('copySVG');
const dlSVGBtn = document.getElementById('downloadSVG');
const dlPNGBtn = document.getElementById('downloadPNG');
const copyJSONBtn = document.getElementById('copyJSON');
const dlJSONBtn = document.getElementById('downloadJSON');
const replayBtn = document.getElementById('replay');
let mode = 'A';
document.querySelectorAll('input[name="mode"]').forEach(r=>{
r.addEventListener('change', ()=>{ mode = document.querySelector('input[name="mode"]:checked').value; });
});
// State
let drawing = false;
let curStroke = null;
const strokes = [];
let t0 = null;
let selectedStroke = null;
let selectedAnchor = {stroke:null, idx:-1};
let dragging = null; // edit-drag
// Pen state
let penActive = false;
let penStroke = null; // {isBezier:true, anchors:[], pathEl,...}
let penDragging = false;
let placingFirst = false;
let placingNext = false;
let penStartPt = null;
function now(){ return performance.now(); }
function svgPoint(evt){
const pt = stage.createSVGPoint();
pt.x = evt.clientX; pt.y = evt.clientY;
const p = pt.matrixTransform(stage.getScreenCTM().inverse());
return {x: Math.round(p.x), y: Math.round(p.y)};
}
function createPathEl(){
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('class','sline');
p.setAttribute('stroke-width', strokeInp.value);
p.addEventListener('pointerdown', (e)=>{
if(editBezierCb.checked && !penActive){ e.stopPropagation(); const s = strokes.find(st=>st.pathEl===p); if(s){ selectStroke(s); drawBezierUI(); } }
});
strokesG.appendChild(p);
return p;
}
function applyStrokeStyle(s){
if(s.mode==='A'){ s.pathEl.setAttribute('stroke', colorAInp.value); }
else if(s.mode==='B'){ s.pathEl.setAttribute('stroke', colorBInp.value); }
else { s.pathEl.setAttribute('stroke','url(#gradA2B)'); }
}
function pathFromAnchors(A){
if(!A || A.length<2) return '';
let d = `M ${A[0].x} ${A[0].y}`;
for(let i=0;i<A.length-1;i++){
const a=A[i], b=A[i+1];
d += ` C ${a.h2.x} ${a.h2.y}, ${b.h1.x} ${b.h1.y}, ${b.x} ${b.y}`;
}
return d;
}
function listRefresh(){
strokeList.innerHTML = '';
strokes.forEach((s,i)=>{
const li = document.createElement('li'); li.className='strokeItem';
const sw = document.createElement('input'); sw.type='range'; sw.min=4; sw.max=28; sw.step=1; sw.value=s.width; sw.title='Breite';
sw.addEventListener('input',()=>{ s.width=+sw.value; s.pathEl.setAttribute('stroke-width', s.width); });
const sel = document.createElement('select');
['A','B','GRAD'].forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; if(s.mode===v) o.selected=true; sel.appendChild(o); });
sel.addEventListener('change',()=>{ s.mode=sel.value; applyStrokeStyle(s); });
const edit = document.createElement('button'); edit.className='btn'; edit.textContent='Bearbeiten';
edit.addEventListener('click',()=>{ selectStroke(s); editBezierCb.checked = true; drawBezierUI(); });
const del = document.createElement('button'); del.className='btn'; del.textContent='✕';
del.addEventListener('click',()=>{ removeStroke(i); });
const meta = document.createElement('span'); meta.className='muted';
meta.textContent = `#${s.id}${s.isBezier? 'Bezier' : (s.points?.length||0)+' pts'}`;
li.append('Stroke', sel, sw, edit, del, meta);
strokeList.appendChild(li);
});
}
function removeStroke(idx){
const s = strokes[idx]; if(!s) return;
s.pathEl.remove(); if(s.startMarker) s.startMarker.remove(); if(s.stopMarker) s.stopMarker.remove();
if(selectedStroke===s){ selectedStroke=null; clearBezierUI(); }
strokes.splice(idx,1);
listRefresh();
}
function selectStroke(s){
selectedStroke = s;
strokes.forEach(st=> st.pathEl.classList.toggle('selected', st===s));
}
// -------- Freehand --------
function toPathD(s){
if(s.isBezier) return pathFromAnchors(s.anchors);
const pts = s.points;
const smp = s.sampler || samplerSel.value;
if(pts.length===0) return '';
if(smp==='raw'){
let d = `M ${pts[0].x} ${pts[0].y}`;
for(let i=1;i<pts.length;i++){ d+=` L ${pts[i].x} ${pts[i].y}`; } return d;
} else if(smp==='cubic'){
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); const P=(i)=>pts[ clamp(i,0,pts.length-1) ];
let d=`M ${pts[0].x} ${pts[0].y}`;
for(let i=0;i<pts.length-1;i++){
const p0=P(i-1), p1=P(i), p2=P(i+1), p3=P(i+2);
const c1x = p1.x + (p2.x - p0.x)/6, c1y = p1.y + (p2.y - p0.y)/6;
const c2x = p2.x - (p3.x - p1.x)/6, c2y = p2.y - (p3.y - p1.y)/6;
d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2.x} ${p2.y}`;
} return d;
} else {
let d = `M ${pts[0].x} ${pts[0].y}`;
for(let i=1;i<pts.length-1;i++){ const p0=pts[i], p1=pts[i+1]; const mx=(p0.x+p1.x)/2, my=(p0.y+p1.y)/2; d += ` Q ${p0.x} ${p0.y} ${mx} ${my}`; }
const last=pts[pts.length-1]; d += ` L ${last.x} ${last.y}`; return d;
}
}
function updatePath(s){ s.pathEl.setAttribute('d', toPathD(s)); s.pathEl.setAttribute('stroke-width', s.width); }
function polyLen(pts){ let L=0; for(let i=1;i<pts.length;i++){ const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; L += Math.hypot(dx,dy);} return L; }
function startStroke(e){
if(penActive || editBezierCb.checked){ return; }
if(bitMode.checked){ placeBit(e); return; }
if(sparkMode.checked || e.shiftKey){ placeSpark(e); return; }
const t = now();
const pt = svgPoint(e);
const pathEl = createPathEl();
curStroke = { id: Date.now()%1e7, mode, colorA: colorAInp.value, colorB: colorBInp.value, width:+strokeInp.value, points:[pt], pathEl, start:t, duration:0, length:0, sampler:samplerSel.value, isBezier:false };
applyStrokeStyle(curStroke); updatePath(curStroke);
if(markersCb.checked){ curStroke.startMarker = mark(pt.x, pt.y, 'start'); }
drawing = true;
}
stage.addEventListener('pointermove', (e)=>{
if(!drawing || !curStroke) return;
const p = svgPoint(e);
const last = curStroke.points[curStroke.points.length-1];
if(!last || Math.hypot(p.x-last.x, p.y-last.y) > 2){
curStroke.points.push(p);
updatePath(curStroke);
}
});
window.addEventListener('pointerup', ()=>{
if(drawing && curStroke){
const pts = curStroke.points;
curStroke.duration = 0;
curStroke.length = polyLen(pts);
if(markersCb.checked){ const last=pts[pts.length-1]; curStroke.stopMarker = mark(last.x,last.y,'stop'); }
strokes.push(curStroke); curStroke = null; listRefresh();
}
drawing=false;
});
function mark(x,y,type){
const r = 7; const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class', type==='start'?'markerStart':'markerStop');
markersG.appendChild(c); return c;
}
function placeSpark(e){
const p = svgPoint(e);
const dx = 10, dy = -10;
const l = document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1', p.x); l.setAttribute('y1', p.y);
l.setAttribute('x2', p.x+dx); l.setAttribute('y2', p.y+dy);
l.setAttribute('stroke', colorSInp.value);
l.setAttribute('stroke-width', Math.max(2, Math.round(parseInt(strokeInp.value,10)*0.66)));
l.setAttribute('stroke-linecap','round');
sparksG.appendChild(l);
}
function placeBit(e){
const p = svgPoint(e);
const h = Math.max(18, parseInt(strokeInp.value,10)*1.2);
const w = Math.max(6, parseInt(strokeInp.value,10)*0.8);
const line1 = document.createElementNS('http://www.w3.org/2000/svg','line');
line1.setAttribute('x1', p.x); line1.setAttribute('y1', p.y-h/2);
line1.setAttribute('x2', p.x); line1.setAttribute('y2', p.y+h/2);
line1.setAttribute('stroke', colorBInp.value);
line1.setAttribute('stroke-width', w);
line1.setAttribute('stroke-linecap', 'round');
bitsG.appendChild(line1);
}
// Editor helpers
function clearBezierUI(){ bezierUI.innerHTML=''; selectedAnchor={stroke:null, idx:-1}; delAnchorBtn.disabled = true; }
function drawBezierUI(){
clearBezierUI();
const s = selectedStroke;
if(!editBezierCb.checked || !s || !s.isBezier) return;
s.anchors.forEach((a,idx)=>{
bezierUI.append(line(a.x, a.y, a.h1.x, a.h1.y, 'handleLine'));
bezierUI.append(line(a.x, a.y, a.h2.x, a.h2.y, 'handleLine'));
const h1 = circle(a.h1.x, a.h1.y, 5, 'handle', (e)=>startDrag('h1', s, idx, e));
const h2 = circle(a.h2.x, a.h2.y, 5, 'handle', (e)=>startDrag('h2', s, idx, e));
bezierUI.append(h1, h2);
const an = circle(a.x, a.y, 6.5, 'anchor', (e)=>{ startDrag('anchor', s, idx, e) });
an.addEventListener('dblclick', (e)=>{ e.stopPropagation(); insertAnchorAt(s, idx); });
an.addEventListener('pointerdown', ()=>{ selectedAnchor={stroke:s,idx}; delAnchorBtn.disabled=false; });
bezierUI.append(an);
});
}
function line(x1,y1,x2,y2,cls){ const l = document.createElementNS('http://www.w3.org/2000/svg','line'); l.setAttribute('x1',x1); l.setAttribute('y1',y1); l.setAttribute('x2',x2); l.setAttribute('y2',y2); l.setAttribute('class',cls); return l; }
function circle(x,y,r,cls,onDown){
const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class',cls);
c.style.touchAction = 'none';
if(onDown) c.addEventListener('pointerdown', (e)=>{ e.stopPropagation(); onDown(e); });
return c;
}
function startDrag(type, s, idx, ev){
ev.preventDefault();
const a = s.anchors[idx];
const p0 = svgPoint(ev);
const dragging = {type, stroke:s, idx, ox:p0.x, oy:p0.y, pid:ev.pointerId};
try { stage.setPointerCapture(ev.pointerId); } catch(e){}
const move = (e)=>{
const pos = svgPoint(e);
const dx = pos.x - dragging.ox, dy = pos.y - dragging.oy;
dragging.ox = pos.x; dragging.oy = pos.y;
if(type==='anchor'){
a.x+=dx; a.y+=dy; a.h1.x+=dx; a.h1.y+=dy; a.h2.x+=dx; a.h2.y+=dy;
}else if(type==='h1'){
a.h1.x+=dx; a.h1.y+=dy;
if(linkHandlesCb.checked && !a.split){
a.h2.x = a.x - (a.h1.x - a.x);
a.h2.y = a.y - (a.h1.y - a.y);
}
}else if(type==='h2'){
a.h2.x+=dx; a.h2.y+=dy;
if(linkHandlesCb.checked && !a.split){
a.h1.x = a.x - (a.h2.x - a.x);
a.h1.y = a.y - (a.h2.y - a.y);
}
}
s.pathEl.setAttribute('d', pathFromAnchors(s.anchors));
drawBezierUI();
};
const up = ()=>{
try { stage.releasePointerCapture(ev.pointerId); } catch(_) {}
window.removeEventListener('pointermove', move);
};
window.addEventListener('pointermove', move, {passive:true});
window.addEventListener('pointerup', up, {once:true});
}
function insertAnchorAt(s, idx){
const A = s.anchors; if(idx>=A.length-1) return;
const a=A[idx], b=A[idx+1];
const mid = {x:(a.x+b.x)/2, y:(a.y+b.y)/2};
const dir = {x:(b.x-a.x), y:(b.y-a.y)};
const hLen = {x:dir.x*0.33, y:dir.y*0.33};
const newA = { x: mid.x, y: mid.y,
h1:{x: mid.x - hLen.x*0.5, y: mid.y - hLen.y*0.5},
h2:{x: mid.x + hLen.x*0.5, y: mid.y + hLen.y*0.5},
split:false };
A.splice(idx+1,0,newA);
s.pathEl.setAttribute('d', pathFromAnchors(A));
drawBezierUI();
}
delAnchorBtn.addEventListener('click', ()=>{
const s=selectedStroke, idx=selectedAnchor.idx;
if(!s || idx<0) return;
if(s.anchors.length<=2) return;
s.anchors.splice(idx,1);
selectedAnchor={stroke:null,idx:-1};
delAnchorBtn.disabled=true;
s.pathEl.setAttribute('d', pathFromAnchors(s.anchors));
drawBezierUI();
});
toBezierBtn.addEventListener('click', ()=>{
if(selectedStroke && !selectedStroke.isBezier){
const s=selectedStroke; const pts=s.points;
if(!pts || pts.length<2) return;
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); const P=(i)=>pts[ clamp(i,0,pts.length-1) ];
const A=[];
for(let i=0;i<pts.length-1;i++){
const p0=P(i-1), p1=P(i), p2=P(i+1), p3=P(i+2);
const c1 = {x: p1.x + (p2.x - p0.x)/6, y: p1.y + (p2.y - p0.y)/6};
const c2 = {x: p2.x - (p3.x - p1.x)/6, y: p2.y - (p3.y - p1.y)/6};
if(i===0){ A.push({x:p1.x,y:p1.y,h1:{x:p1.x,y:p1.y},h2:{x:c1.x,y:c1.y},split:false}); }
else { A[A.length-1].h2 = {x:c1.x,y:c1.y}; }
A.push({x:p2.x,y:p2.y,h1:{x:c2.x,y:c2.y},h2:{x:p2.x,y:p2.y},split:false});
}
s.anchors=A; s.isBezier=true;
s.pathEl.setAttribute('d', pathFromAnchors(A));
listRefresh(); drawBezierUI();
}
});
splitBtn.addEventListener('click', ()=>{
if(selectedStroke && selectedAnchor.idx>=0){ const a=selectedStroke.anchors[selectedAnchor.idx]; a.split=!a.split; drawBezierUI(); }
});
window.addEventListener('keydown', (e)=>{
if((e.key==='Delete'||e.key==='Backspace') && penActive){ penBackspace(); }
if((e.key==='Delete'||e.key==='Backspace') && editBezierCb.checked){ delAnchorBtn.click(); }
if(e.key==='l' || e.key==='L'){ linkHandlesCb.checked = !linkHandlesCb.checked; }
if(e.key==='Enter' && penActive){ finishPen(); }
});
// Pen tool
penModeCb.addEventListener('change', ()=>{
const on = penModeCb.checked;
if(on){ editBezierCb.checked=false; penActive=true; ghost.setAttribute('d',''); deselectAll(); }
else { penActive=false; cancelPen(); }
finishPenBtn.disabled = !on;
});
finishPenBtn.addEventListener('click', finishPen);
function startPen(e){
const p = svgPoint(e);
if(!penStroke){
penStroke = { id: Date.now()%1e7, mode, width:+strokeInp.value, colorA: colorAInp.value, colorB: colorBInp.value,
isBezier:true, anchors:[], pathEl:createPathEl() };
applyStrokeStyle(penStroke);
const a = {x:p.x,y:p.y,h1:{x:p.x,y:p.y},h2:{x:p.x,y:p.y},split:false};
penStroke.anchors.push(a);
placingFirst=true; penDragging=true; penStartPt=p;
try{ stage.setPointerCapture(e.pointerId); }catch(_){}}
else {
const a = {x:p.x,y:p.y,h1:{x:p.x,y:p.y},h2:{x:p.x,y:p.y},split:false};
penStroke.anchors.push(a);
placingNext=true; penDragging=true;
try{ stage.setPointerCapture(e.pointerId); }catch(_){}}
}
function movePen(e){
if(!penStroke || !penDragging) return;
const p = svgPoint(e);
const A = penStroke.anchors;
if(placingFirst){
const start=A[0];
let v = {x: p.x - start.x, y: p.y - start.y};
if(e.shiftKey){
const ang=Math.atan2(v.y,v.x), step=Math.PI/12; const a=Math.round(ang/step)*step; const L=Math.hypot(v.x,v.y); v={x:Math.cos(a)*L,y:Math.sin(a)*L};
}
start.h2 = {x:start.x+v.x, y:start.y+v.y};
} else if(placingNext){
const last=A[A.length-1], prev=A[A.length-2];
let v = {x: p.x - last.x, y: p.y - last.y};
if(e.shiftKey){
const ang=Math.atan2(v.y,v.x), step=Math.PI/12; const a=Math.round(ang/step)*step; const L=Math.hypot(v.x,v.y); v={x:Math.cos(a)*L,y:Math.sin(a)*L};
}
last.h1 = {x:last.x+v.x, y:last.y+v.y};
if(e.altKey){ last.split=true; last.h2={x:last.x,y:last.y}; }
else { last.split=false; last.h2 = {x:last.x - v.x, y:last.y - v.y}; }
// previous outgoing 1/3 along segment
const seg = {x:last.x - prev.x, y:last.y - prev.y};
const L = Math.hypot(seg.x,seg.y); if(L>0){
const n={x:seg.x/L,y:seg.y/L}; const k=L/3;
prev.h2 = {x: prev.x + n.x*k, y: prev.y + n.y*k};
}
}
ghost.setAttribute('d', pathFromAnchors(A));
penStroke.pathEl.setAttribute('d', pathFromAnchors(A));
applyStrokeStyle(penStroke);
penStroke.pathEl.setAttribute('stroke-width', penStroke.width);
}
function endPen(e){ penDragging=false; placingFirst=false; placingNext=false; ghost.setAttribute('d',''); }
function finishPen(){ if(!penStroke) return; strokes.push(penStroke); listRefresh(); penStroke=null; penStartPt=null; ghost.setAttribute('d',''); }
function cancelPen(){ if(penStroke){ penStroke.pathEl.remove(); } penStroke=null; penStartPt=null; ghost.setAttribute('d',''); }
function penBackspace(){ if(!penStroke) return; const A=penStroke.anchors; if(A.length<=1){ cancelPen(); } else { A.pop(); penStroke.pathEl.setAttribute('d', pathFromAnchors(A)); } }
// Stage routing
stage.addEventListener('pointerdown', (e)=>{ if(penModeCb.checked) startPen(e); else startStroke(e); });
stage.addEventListener('pointermove', (e)=>{ if(penModeCb.checked) movePen(e); });
window.addEventListener('pointerup', (e)=>{ if(penModeCb.checked) endPen(e); });
// Misc
function exportSVGString(){
frameRect.style.display = frameOnCb.checked ? 'block' : 'none';
const clone = stage.cloneNode(true);
const ui = clone.querySelector('#bezierUI'); if(ui) ui.remove();
const gh = clone.querySelector('#ghost'); if(gh) gh.remove();
const s = new XMLSerializer().serializeToString(clone);
return `<?xml version="1.0" encoding="UTF-8"?>\n${s}`;
}
function motionJSON(){
const items = strokes.map(s=>{
const o = { id:s.id, mode:s.mode, colorA:s.colorA, colorB:s.colorB, width:s.width, sampler:s.sampler || 'cubic' };
if(s.isBezier){
o.isBezier = true;
o.anchors = s.anchors.map(a=>({x:a.x,y:a.y,h1:{x:a.h1.x,y:a.h1.y},h2:{x:a.h2.x,y:a.h2.y},split:!!a.split}));
}else{
o.points = (s.points||[]).map(p=>({x:p.x,y:p.y}));
}
return o;
});
return JSON.stringify({strokes:items}, null, 2);
}
document.getElementById('copySVG').addEventListener('click', async ()=>{ const svg=exportSVGString(); await navigator.clipboard.writeText(svg); alert('SVG kopiert'); });
document.getElementById('downloadSVG').addEventListener('click', ()=>{ const svg=exportSVGString(); const blob=new Blob([svg],{type:'image/svg+xml'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega-oneliner.svg'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
document.getElementById('downloadPNG').addEventListener('click', ()=>{ const svg=exportSVGString(); svgToPng(svg, 'omi-omega-oneliner.png'); });
document.getElementById('copyJSON').addEventListener('click', async ()=>{ const js=motionJSON(); await navigator.clipboard.writeText(js); alert('MotionJSON kopiert'); });
document.getElementById('downloadJSON').addEventListener('click', ()=>{ const js=motionJSON(); const blob=new Blob([js],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega_motion.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
document.getElementById('replay').addEventListener('click', ()=>{
if(!strokes.length) return;
strokes.forEach(s=> s.pathEl.setAttribute('d',''));
const total=1500; const t0=performance.now();
const step=()=>{
const t=performance.now()-t0;
strokes.forEach(s=>{
if(!s.isBezier) return;
const A=s.anchors, n=A.length-1; const k=Math.max(1, Math.floor((t/total)*n));
let d=`M ${A[0].x} ${A[0].y}`;
for(let i=0;i<k && i<n;i++){ const a=A[i], b=A[i+1]; d+=` C ${a.h2.x} ${a.h2.y}, ${b.h1.x} ${b.h1.y}, ${b.x} ${b.y}`; }
s.pathEl.setAttribute('d', d); applyStrokeStyle(s); s.pathEl.setAttribute('stroke-width', s.width);
});
if(t<total) requestAnimationFrame(step); else { strokes.forEach(updatePath); }
};
requestAnimationFrame(step);
});
function svgToPng(svgString, filename){
const img = new Image();
const svgBlob = new Blob([svgString], {type:'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(svgBlob);
img.onload = function(){
const canvas = document.createElement('canvas');
const vb = stage.viewBox.baseVal;
canvas.width = vb.width; canvas.height = vb.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob)=>{
const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=filename; a.click();
});
URL.revokeObjectURL(url);
};
img.src = url;
}
// Basic toggles
frameRect.style.display = document.getElementById('frameOn').checked ? 'block' : 'none';
document.getElementById('frameOn').addEventListener('change', (e)=>{ frameRect.style.display = e.target.checked ? 'block' : 'none'; });
document.getElementById('undo').addEventListener('click', ()=>{ if(strokes.length){ removeStroke(strokes.length-1); }});
document.getElementById('clear').addEventListener('click', ()=>{ strokes.length=0; strokesG.innerHTML=''; bitsG.innerHTML=''; sparksG.innerHTML=''; markersG.innerHTML=''; ghost.setAttribute('d',''); clearBezierUI(); listRefresh(); });
function deselectAll(){ selectedStroke=null; strokes.forEach(st=> st.pathEl.classList.remove('selected')); clearBezierUI(); }
})();</script>
</body>
</html>

View File

@@ -0,0 +1,452 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Schnippsi Painter — Stream + Bezier</title>
<style>
:root{
--bg:#0b0f12; --panel:#12181f; --ink:#e2f2ff; --muted:#7a8aa0; --accent:#62d3a4; --accent2:#ffd166;
}
html,body{height:100%;margin:0;background:var(--bg);color:var(--ink);font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
#wrap{display:grid;grid-template-rows:auto 1fr; height:100%;}
#toolbar{
display:flex;gap:.75rem;align-items:center; padding:.6rem .8rem; background:linear-gradient(180deg,#12181f,#0e141a);
position:sticky; top:0; z-index:3; box-shadow:0 8px 20px rgba(0,0,0,.25);
}
#toolbar input[type=color], #toolbar input[type=text]{height:32px}
#toolbar .btn{
padding:.55rem .8rem; border-radius:14px; background:#1a2330; color:#cfe7ff; border:1px solid #2b3a4b; cursor:pointer;
}
#toolbar .btn[aria-pressed="true"]{background:linear-gradient(180deg,#1f2a36,#1a2330); outline:2px solid var(--accent);}
#toolbar .danger{border-color:#603; color:#ffd9e6; background:#2a0f1f}
#toolbar .ok{border-color:#1b5e3b; background:#0f241a; color:#d9ffef;}
#toolbar .field{display:flex;align-items:center;gap:.4rem}
#status{margin-left:auto; font-size:.9rem; color:var(--muted)}
#canvasWrap{position:relative; height:100%;}
canvas{position:absolute; inset:0; width:100%; height:100%; touch-action:none; background:#0a0d11; outline:none}
#help{
position:fixed; right:.8rem; bottom:.8rem; width:360px; max-width:95vw; background:#0e141a; border:1px solid #213041;
border-radius:16px; padding:12px 14px; color:#cfe7ff; box-shadow:0 18px 40px rgba(0,0,0,.45); font-size:14px;
}
#help h3{margin:.2rem 0 .4rem; font-size:16px}
#help kbd{background:#111a22; padding:.15rem .35rem; border-radius:6px; border:1px solid #2b3a4b}
.pill{padding:.08rem .45rem; background:#13202a; border:1px solid #254257; border-radius:999px; font-size:12px; color:#bfe8ff}
.handle{cursor:grab}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
</style>
</head>
<body>
<div id="wrap">
<div id="toolbar" role="toolbar" aria-label="Zeichenwerkzeuge">
<div class="field"><span class="pill">Tool</span>
<button class="btn" id="toolFree" aria-pressed="true" aria-label="Freihand (F)">Freihand</button>
<button class="btn" id="toolBezier" aria-pressed="false" aria-label="Bezier (B)">Bezier</button>
</div>
<div class="field"><span class="pill">Farbe A</span><input id="colA" type="color" value="#62d3a4"/></div>
<div class="field"><span class="pill">Farbe B</span><input id="colB" type="color" value="#ffd166"/></div>
<div class="field"><span class="pill">Breite</span><input id="width" type="range" min="1" max="48" value="12"/></div>
<div class="field"><span class="pill">Stream</span>
<input id="wsUrl" type="text" size="22" value="ws://localhost:9980" aria-label="WebSocket URL"/>
<button class="btn" id="wsToggle" aria-pressed="false" aria-label="Streaming an/aus (S)">Verbinden</button>
</div>
<div class="field">
<button class="btn" id="replayBtn" aria-label="Replay (R)">Replay</button>
<button class="btn" id="exportJson">JSON</button>
<button class="btn" id="exportSvg">SVG</button>
<button class="btn" id="exportPng">PNG</button>
<button class="btn danger" id="clearBtn">Clear</button>
</div>
<div id="status" aria-live="polite">bereit</div>
</div>
<div id="canvasWrap">
<canvas id="cv" role="application" aria-label="Schnippsi Zeichenfläche" tabindex="0"></canvas>
</div>
</div>
<aside id="help" aria-live="polite">
<h3>Shortcuts</h3>
<div>Hilfe <kbd>?</kbd> / <kbd>H</kbd> • Tool-Bezier <kbd>B</kbd> • Tool-Freihand <kbd>F</kbd> • Stream <kbd>S</kbd></div>
<div>Bezier beenden <kbd>Enter</kbd> • Letzten Anker löschen <kbd>Backspace</kbd> • Ecke halten <kbd>Alt</kbd> • Snap <kbd>Shift</kbd></div>
<div class="pill">A11y</div> Tab-fokussierbar, ARIA Live-Status, hoher Kontrast, 200% Zoom stabil.
</aside>
<script>
(()=>{
const dpr = Math.max(1, window.devicePixelRatio || 1);
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const statusEl = document.getElementById('status');
const toolFree = document.getElementById('toolFree');
const toolBezier = document.getElementById('toolBezier');
const colA = document.getElementById('colA');
const colB = document.getElementById('colB');
const widthIn = document.getElementById('width');
const replayBtn = document.getElementById('replayBtn');
const exportJson = document.getElementById('exportJson');
const exportSvg = document.getElementById('exportSvg');
const exportPng = document.getElementById('exportPng');
const clearBtn = document.getElementById('clearBtn');
const wsToggle = document.getElementById('wsToggle');
const wsUrl = document.getElementById('wsUrl');
let state = {
tool: 'free', // 'free' | 'bezier'
colorA: colA.value,
colorB: colB.value,
width: +widthIn.value,
strokes: [],
current: null,
sessionId: ('sess_' + Math.random().toString(16).slice(2)),
streaming: false,
ws: null,
down: false,
modifiers: {shift:false, alt:false},
bezier: {anchors:[], dragging:null}
};
function resize(){
const rect = cv.getBoundingClientRect();
cv.width = Math.floor(rect.width * dpr);
cv.height = Math.floor(rect.height * dpr);
ctx.setTransform(dpr,0,0,dpr,0,0);
render();
}
new ResizeObserver(resize).observe(document.getElementById('canvasWrap'));
resize();
function setStatus(t){ statusEl.textContent = t; }
// WebSocket
function wsConnect(){
try{
const url = wsUrl.value.trim();
state.ws = new WebSocket(url);
state.ws.onopen = ()=>{ state.streaming = true; wsToggle.setAttribute('aria-pressed','true'); setStatus('WS verbunden'); send({type:'session', id:state.sessionId, app:'schnippsi-painter', t:performance.now()|0}); };
state.ws.onclose = ()=>{ state.streaming = false; wsToggle.setAttribute('aria-pressed','false'); setStatus('WS getrennt'); };
state.ws.onerror = ()=>{ setStatus('WS Fehler'); };
wsToggle.textContent = 'Trennen';
}catch(e){ console.error(e); setStatus('WS Verbindungsfehler'); }
}
function wsDisconnect(){
if(state.ws){ try{ state.ws.close(); }catch(e){} }
state.ws = null; state.streaming = false; wsToggle.textContent = 'Verbinden';
}
function send(obj){
if(!state.streaming || !state.ws || state.ws.readyState!==1) return;
try{ state.ws.send(JSON.stringify(obj)); }catch(e){ /* ignore */ }
}
// Tools
function useTool(t){
state.tool = t;
toolFree.setAttribute('aria-pressed', t==='free');
toolBezier.setAttribute('aria-pressed', t==='bezier');
setStatus(t==='free'?'Freihand aktiv':'Bezier aktiv');
}
toolFree.onclick = ()=>useTool('free');
toolBezier.onclick = ()=>useTool('bezier');
colA.oninput = ()=>{ state.colorA = colA.value; };
colB.oninput = ()=>{ state.colorB = colB.value; };
widthIn.oninput = ()=>{ state.width = +widthIn.value; };
wsToggle.onclick = ()=>{
if(state.streaming) wsDisconnect();
else wsConnect();
};
clearBtn.onclick = ()=>{ state.strokes.length=0; state.bezier.anchors=[]; state.current=null; render(); };
// Geometry helpers
const dist = (a,b)=>Math.hypot(a.x-b.x, a.y-b.y);
function lerp(a,b,t){ return a+(b-a)*t; }
function cubicPoint(p0,p1,p2,p3,t){
const x = Math.pow(1-t,3)*p0.x + 3*Math.pow(1-t,2)*t*p1.x + 3*(1-t)*t*t*p2.x + t*t*t*p3.x;
const y = Math.pow(1-t,3)*p0.y + 3*Math.pow(1-t,2)*t*p1.y + 3*(1-t)*t*t*p2.y + t*t*t*p3.y;
return {x,y};
}
// Drawing primitives
function drawStroke(s){
ctx.lineJoin = ctx.lineCap = 'round';
ctx.lineWidth = s.width;
ctx.strokeStyle = s.colorA;
ctx.globalAlpha = 1;
ctx.beginPath();
if(s.isBezier){
const A = s.anchors;
if(A.length>0){ ctx.moveTo(A[0].x, A[0].y); }
for(let i=1;i<A.length;i++){
const a0=A[i-1], a1=A[i];
ctx.bezierCurveTo(a0.h2.x, a0.h2.y, a1.h1.x, a1.h1.y, a1.x, a1.y);
}
ctx.stroke();
// Handles (edit mode)
if(s._edit){
for(const a of A){
// anchor
ctx.fillStyle = '#cfe7ff';
ctx.beginPath(); ctx.arc(a.x,a.y,4,0,Math.PI*2); ctx.fill();
// handles
ctx.strokeStyle='#2b9ad1'; ctx.lineWidth=1.2;
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(a.h1.x,a.h1.y); ctx.moveTo(a.x,a.y); ctx.lineTo(a.h2.x,a.h2.y); ctx.stroke();
ctx.fillStyle='#2b9ad1';
ctx.beginPath(); ctx.arc(a.h1.x,a.h1.y,3,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(a.h2.x,a.h2.y,3,0,Math.PI*2); ctx.fill();
}
}
}else{
const P = s.points;
if(!P || P.length<2) return;
ctx.moveTo(P[0].x, P[0].y);
for(let i=1;i<P.length;i++){ ctx.lineTo(P[i].x, P[i].y); }
ctx.stroke();
}
}
function render(){
ctx.clearRect(0,0,cv.width,dpr?cv.height:cv.height);
// backdrop grid (subtle)
ctx.save();
ctx.strokeStyle='rgba(255,255,255,0.04)';
ctx.lineWidth=1;
for(let x=0;x<cv.width/dpr;x+=40){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,cv.height/dpr); ctx.stroke(); }
for(let y=0;y<cv.height/dpr;y+=40){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(cv.width/dpr,y); ctx.stroke(); }
ctx.restore();
for(const s of state.strokes){ drawStroke(s); }
if(state.current){ drawStroke(state.current); }
}
// Input
let lastEmit = 0;
function pointerPos(e){
const r = cv.getBoundingClientRect();
return { x:(e.clientX - r.left), y:(e.clientY - r.top), t: performance.now()|0, p:e.pressure||0.5, tilt:[e.tiltX||0, e.tiltY||0], type:e.pointerType||'mouse' };
}
function startStroke(pt){
if(state.tool==='free'){
state.current = { id:('st_'+Date.now()), tool:'free', colorA:state.colorA, colorB:state.colorB, width:state.width, points:[pt], isBezier:false };
send({type:'strokeStart', id:state.current.id, tool:'free', colorA:state.colorA, colorB:state.colorB, width:state.width, t:pt.t});
}else{
// initialize bezier edit stroke
const A = state.bezier.anchors.length? structuredClone(state.bezier.anchors) : [];
state.current = { id:('st_'+Date.now()), tool:'bezier', colorA:state.colorA, colorB:state.colorB, width:state.width, anchors:A, isBezier:true, _edit:true };
if(A.length===0){
// first anchor with mirrored handles
const a = {x:pt.x,y:pt.y, h1:{x:pt.x-40, y:pt.y}, h2:{x:pt.x+40, y:pt.y}, split:false, t:pt.t, p:pt.p, tilt:pt.tilt};
state.bezier.anchors.push(a);
state.current.anchors.push(structuredClone(a));
send({type:'strokeStart', id:state.current.id, tool:'bezier', colorA:state.colorA, colorB:state.colorB, width:state.width, t:pt.t});
send({type:'anchor', id:state.current.id, a:a});
}
}
}
function moveStroke(pt){
if(!state.current) return;
if(state.tool==='free'){
state.current.points.push(pt);
const now = performance.now();
if(now-lastEmit>33){ lastEmit=now; send({type:'point', id:state.current.id, p:pt}); }
}else{
// handle dragging logic
const cur = state.current;
if(!state.bezier.dragging) return;
const drag = state.bezier.dragging;
if(drag.kind==='anchor'){
drag.ref.x = pt.x; drag.ref.y = pt.y;
// move handles with anchor
drag.ref.h1.x += drag.dx? (pt.x-drag.base.x) : 0;
drag.ref.h1.y += drag.dy? (pt.y-drag.base.y) : 0;
drag.ref.h2.x += drag.dx? (pt.x-drag.base.x) : 0;
drag.ref.h2.y += drag.dy? (pt.y-drag.base.y) : 0;
drag.base.x = pt.x; drag.base.y = pt.y;
}else if(drag.kind==='h1' || drag.kind==='h2'){
drag.ref[drag.kind].x = pt.x; drag.ref[drag.kind].y = pt.y;
if(!drag.ref.split){
// mirror handle lengths for smooth
const ax=drag.ref.x, ay=drag.ref.y;
const opp = (drag.kind==='h1')?'h2':'h1';
const vx = ax - pt.x, vy = ay - pt.y;
drag.ref[opp].x = ax + vx; drag.ref[opp].y = ay + vy;
}
}
cur.anchors = structuredClone(state.bezier.anchors);
// emit throttle
const now = performance.now();
if(now-lastEmit>50){ lastEmit=now; send({type:'anchors', id:cur.id, anchors:cur.anchors}); }
}
render();
}
function endStroke(pt){
if(!state.current) return;
if(state.tool==='free'){
state.strokes.push(state.current);
send({type:'strokeEnd', id:state.current.id, t:pt.t});
state.current=null;
}else{
// in bezier tool, mouseup only stops dragging
state.bezier.dragging=null;
}
render();
}
// Hit-testing for bezier edit
function hitTestAnchors(x,y){
const A = state.bezier.anchors;
for(let i=A.length-1;i>=0;i--){
const a=A[i];
if(dist({x,y}, a)<8) return {idx:i, kind:'anchor', ref:a};
if(dist({x,y}, a.h1)<7) return {idx:i, kind:'h1', ref:a};
if(dist({x,y}, a.h2)<7) return {idx:i, kind:'h2', ref:a};
}
return null;
}
// Add anchor with dragging of handle based on movement
function addAnchor(pt){
const A = state.bezier.anchors;
const prev = A[A.length-1];
const a = {x:pt.x,y:pt.y, h1:{x:pt.x-40, y:pt.y}, h2:{x:pt.x+40, y:pt.y}, split:false, t:pt.t, p:pt.p, tilt:pt.tilt};
if(prev){
// make smooth by default: h1 points to previous, h2 mirrored
a.h1 = {x: lerp(prev.x, pt.x, .33), y: lerp(prev.y, pt.y, .33)};
a.h2 = {x: lerp(pt.x, prev.x, .33), y: lerp(pt.y, prev.y, .33)};
}
A.push(a);
if(state.current){ state.current.anchors = structuredClone(A); }
send({type:'anchor', id:state.current?state.current.id:('st_'+Date.now()), a:a});
render();
}
// Pointer events
cv.addEventListener('pointerdown', (e)=>{
cv.setPointerCapture(e.pointerId);
const pt = pointerPos(e); state.down=true;
if(state.tool==='free'){
startStroke(pt);
}else{
// bezier: check hit on existing handle/anchor first
const hit = hitTestAnchors(pt.x,pt.y);
if(hit){
state.bezier.dragging={...hit, base:{x:pt.x,y:pt.y}, dx:true, dy:true};
if(hit.kind!=='anchor' && e.altKey){ hit.ref.split = true; } // allow cusp with Alt while grabbing
}else{
if(!state.current){ startStroke(pt); }
addAnchor(pt);
}
}
render();
});
cv.addEventListener('pointermove', (e)=>{
if(!state.down && state.tool==='bezier'){
// hover cursor
const pt = pointerPos(e), hit = hitTestAnchors(pt.x,pt.y);
cv.style.cursor = hit ? 'grab' : 'crosshair';
}
if(!state.down) return;
moveStroke(pointerPos(e));
});
cv.addEventListener('pointerup', (e)=>{ state.down=false; endStroke(pointerPos(e)); });
cv.addEventListener('pointercancel', (e)=>{ state.down=false; endStroke(pointerPos(e)); });
// Keyboard
window.addEventListener('keydown', (e)=>{
if(e.key==='Shift') state.modifiers.shift=true;
if(e.key==='Alt') state.modifiers.alt=true;
if(e.key==='b' || e.key==='B'){ useTool('bezier'); }
if(e.key==='f' || e.key==='F'){ useTool('free'); }
if(e.key==='s' || e.key==='S'){ state.streaming?wsDisconnect():wsConnect(); }
if(e.key==='r' || e.key==='R'){ doReplay(); }
if(e.key==='Enter' && state.tool==='bezier'){
// finalize bezier stroke into strokes
if(state.current){
const finalized = {...state.current};
finalized._edit=false;
state.strokes.push(finalized);
send({type:'strokeEnd', id:finalized.id, t:performance.now()|0});
state.current=null;
state.bezier.anchors = [];
render();
}
}
if(e.key==='Backspace' && state.tool==='bezier'){
if(state.bezier.anchors.length>0){
const a = state.bezier.anchors.pop();
send({type:'anchorDel', id:state.current?state.current.id:null, aidx: state.bezier.anchors.length});
if(state.current) state.current.anchors = structuredClone(state.bezier.anchors);
render();
}
e.preventDefault();
}
if(e.key==='?' || e.key==='h' || e.key==='H'){
const el=document.getElementById('help'); el.hidden = !el.hidden;
}
});
window.addEventListener('keyup', (e)=>{
if(e.key==='Shift') state.modifiers.shift=false;
if(e.key==='Alt') state.modifiers.alt=false;
});
// Export
function download(name, dataUrl){
const a=document.createElement('a'); a.href=dataUrl; a.download=name; a.click();
}
exportJson.onclick = ()=>{
const payload = {
canvas:{w:cv.width/dpr, h:cv.height/dpr, dpr},
session:{id:state.sessionId, app:'schnippsi-painter'},
strokes: state.strokes
};
const blob = new Blob([JSON.stringify(payload,null,2)], {type:'application/json'});
download(`schnippsi_${Date.now()}.json`, URL.createObjectURL(blob));
};
exportSvg.onclick = ()=>{
const w=cv.width/dpr, h=cv.height/dpr;
function pathFromStroke(s){
if(s.isBezier){
const A=s.anchors; if(A.length<1) return '';
let d=`M ${A[0].x} ${A[0].y}`;
for(let i=1;i<A.length;i++){
const a0=A[i-1], a1=A[i];
d+=` C ${a0.h2.x} ${a0.h2.y}, ${a1.h1.x} ${a1.h1.y}, ${a1.x} ${a1.y}`;
}
return `<path d="${d}" stroke="${s.colorA}" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="${s.width}"/>`;
}else{
const P=s.points; if(!P || P.length<2) return '';
const d='M '+P.map((p,i)=> (i? 'L '+p.x+' '+p.y : p.x+' '+p.y)).join(' ');
return `<path d="${d}" stroke="${s.colorA}" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="${s.width}"/>`;
}
}
const body = state.strokes.map(pathFromStroke).join('\n');
const svg = `<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">\n${body}\n</svg>`;
const blob = new Blob([svg], {type:'image/svg+xml'});
download(`schnippsi_${Date.now()}.svg`, URL.createObjectURL(blob));
};
exportPng.onclick = ()=>{
const url = cv.toDataURL('image/png');
download(`schnippsi_${Date.now()}.png`, url);
};
// Replay
function doReplay(){
if(state.strokes.length===0) return;
const copy = JSON.parse(JSON.stringify(state.strokes));
state.strokes = [];
let idx=0;
function step(){
if(idx>=copy.length) return;
state.strokes.push(copy[idx++]); render();
setTimeout(step, 300);
}
step();
}
replayBtn.onclick = doReplay;
// Initial tip
setStatus('Bezier: Klicken für Anker, ziehen für Griffe. Enter = fertig. Alt zum Brechen, Shift für Snap.');
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,366 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Crumbblocks Wasserkocher (Demo)</title>
<!-- Feste Blockly-Version (verhindert textToDom-Fehler) -->
<script src="https://unpkg.com/blockly@9.3.3/blockly.min.js"></script>
<script src="https://unpkg.com/blockly@9.3.3/msg/de.js"></script>
<style>
:root {
--bg: #0b1016;
--panel: #0e141b;
--ink: #e6edf3;
--muted: #93a1b3;
--accent: #4caf50;
--border: #1f2a37;
--console: #0a0f14;
--console-ink: #b7ffb2;
}
html, body {
height: 100%;
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
}
.wrap {
display: grid;
grid-template-rows: auto auto 1fr auto;
gap: 12px;
max-width: 1200px;
margin: 0 auto;
padding: 16px;
}
header h1 { font-size: 20px; margin: 0; }
.controls {
display: flex; gap: 8px; flex-wrap: wrap;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px; padding: 10px;
}
button {
background: var(--accent);
color: #fff;
border: 0;
border-radius: 8px;
padding: 10px 14px;
font-weight: 700;
cursor: pointer;
}
button.secondary { background: #374151; }
button:active { transform: translateY(1px); }
.stage {
display: grid; grid-template-rows: 64vh 28vh; gap: 12px;
}
#blocklyDiv {
width: 100%;
height: 64vh;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}
#output {
width: 100%;
height: 28vh;
background: var(--console);
color: var(--console-ink);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
margin: 0;
overflow: auto;
white-space: pre-wrap;
}
.hint {
color: var(--muted);
font-size: 12px;
margin-top: 6px;
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>🧁 Crumbblocks Virtueller Wasserkocher (Kids-Demo)</h1>
<div class="hint">
Variablen: <code>power_w</code>, <code>wasser_ml</code>, <code>sensor_temp_c</code>, <code>zeit_s</code>,
<code>energie_wh</code>, <code>verbrauch_wh</code>. &nbsp; Ziel: Bis 100 °C erhitzen, Energie/Zeit mitrechnen.
</div>
</header>
<div class="controls">
<button onclick="loadDemo()">🎬 Demo laden</button>
<button onclick="runCode()">▶️ Ausführen</button>
<button class="secondary" onclick="clearOutput()">🧹 Leeren</button>
<span class="hint">Sendet per <code>POST /crumbapi/blockly-terminal</code> (Fallback: Echo).</span>
</div>
<div class="stage">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Terminal wartet …</pre>
</div>
<!-- TOOLBOX (Standardkategorien + Variablen/Funktionen) -->
<xml id="toolbox" style="display:none">
<category name="Logik" categorystyle="logic_category">
<block type="controls_if"></block>
<block type="logic_compare"></block>
<block type="logic_operation"></block>
<block type="logic_boolean"></block>
</category>
<category name="Schleifen" categorystyle="loop_category">
<block type="controls_repeat_ext"><value name="TIMES"><block type="math_number"><field name="NUM">10</field></block></value></block>
<block type="controls_whileUntil"><field name="MODE">WHILE</field></block>
<block type="controls_flow_statements"></block>
</category>
<category name="Mathe" categorystyle="math_category">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_round"></block>
</category>
<category name="Text" categorystyle="text_category">
<block type="text"></block>
<block type="text_print"></block>
</category>
<sep></sep>
<category name="Variablen" categorystyle="variable_category" custom="VARIABLE_DYNAMIC"></category>
<category name="Funktionen" categorystyle="procedure_category" custom="PROCEDURE"></category>
</xml>
<!-- DEMO-WORKSPACE (Wasserkocher) -->
<script id="demo-xml" type="text/plain">
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>sensor_temp_c</variable>
<variable>power_w</variable>
<variable>wasser_ml</variable>
<variable>zeit_s</variable>
<variable>energie_wh</variable>
<variable>verbrauch_wh</variable>
</variables>
<!-- Startwerte -->
<block type="variables_set" x="24" y="24">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE"><block type="math_number"><field name="NUM">20</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">power_w</field>
<value name="VALUE"><block type="math_number"><field name="NUM">2000</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">wasser_ml</field>
<value name="VALUE"><block type="math_number"><field name="NUM">250</field></block></value>
<next>
<!-- Sicherheitslimit Wasser -->
<block type="controls_if">
<value name="IF0">
<block type="logic_compare">
<field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">wasser_ml</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">2000</field></block></value>
</block>
</value>
<statement name="DO0">
<block type="variables_set">
<field name="VAR">wasser_ml</field>
<value name="VALUE"><block type="math_number"><field name="NUM">2000</field></block></value>
</block>
</statement>
<next>
<block type="variables_set">
<field name="VAR">zeit_s</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">energie_wh</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">verbrauch_wh</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<!-- while (sensor_temp_c < 100) -->
<block type="controls_whileUntil">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare">
<field name="OP">LT</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">100</field></block></value>
</block>
</value>
<statement name="DO">
<!-- Abbruch wenn kein Strom -->
<block type="controls_if">
<value name="IF0">
<block type="logic_compare">
<field name="OP">LTE</field>
<value name="A"><block type="variables_get"><field name="VAR">power_w</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">0</field></block></value>
</block>
</value>
<statement name="DO0">
<block type="text_print">
<value name="TEXT"><block type="text"><field name="TEXT">Kein Strom STOP</field></block></value>
<next>
<block type="controls_flow_statements"><field name="FLOW">BREAK</field></block>
</next>
</block>
</statement>
<next>
<!-- Heize-Schritt -->
<block type="text_print">
<value name="TEXT"><block type="text"><field name="TEXT">Heize</field></block></value>
<next>
<!-- sensor_temp_c += 5 -->
<block type="variables_set">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">5</field></block></value>
</block>
</value>
<next>
<!-- zeit_s += 5 -->
<block type="variables_set">
<field name="VAR">zeit_s</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">zeit_s</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">5</field></block></value>
</block>
</value>
<next>
<!-- energie_wh += power_w * 5 / 3600 -->
<block type="variables_set">
<field name="VAR">energie_wh</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">energie_wh</field></block></value>
<value name="B">
<block type="math_arithmetic">
<field name="OP">DIVIDE</field>
<value name="A">
<block type="math_arithmetic">
<field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">power_w</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">5</field></block></value>
</block>
</value>
<value name="B"><block type="math_number"><field name="NUM">3600</field></block></value>
</block>
</value>
</block>
</value>
<next>
<block type="variables_set">
<field name="VAR">verbrauch_wh</field>
<value name="VALUE"><block type="variables_get"><field name="VAR">energie_wh</field></block></value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</statement>
<next>
<block type="text_print">
<value name="TEXT"><block type="text"><field name="TEXT">Fertig. STOP.</field></block></value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</xml>
</script>
<script>
// Workspace
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'geras',
grid: { spacing: 24, length: 3, colour: '#223045', snap: true },
zoom: { controls: true, wheel: true, startScale: 1.05, maxScale: 2.0, minScale: 0.5, pinch: true },
trashcan: true
});
window.addEventListener('resize', () => Blockly.svgResize(workspace));
function out(txt, replace=false) {
const el = document.getElementById('output');
el.textContent = replace ? String(txt) : (el.textContent + '\n' + String(txt));
el.scrollTop = el.scrollHeight;
}
function clearOutput() { document.getElementById('output').textContent = '🌲 Terminal wartet …'; }
function loadDemo() {
try {
const xmlTxt = document.getElementById('demo-xml').textContent.trim();
const dom = (Blockly.Xml && Blockly.Xml.textToDom)
? Blockly.Xml.textToDom(xmlTxt)
: Blockly.utils.xml.textToDom(xmlTxt);
workspace.clear();
Blockly.Xml.domToWorkspace(dom, workspace);
out('✅ Demo geladen.', true);
} catch (e) {
out('❌ Demo konnte nicht geladen werden: ' + e, true);
}
}
async function runCode() {
const code = Blockly.JavaScript.workspaceToCode(workspace);
out('📤 Sende an Terminal…\n\n' + code, true);
// Variablenliste (für spätere Auswertung im Backend hilfreich)
const knownVars = ['sensor_temp_c','power_w','wasser_ml','zeit_s','energie_wh','verbrauch_wh'];
try {
const res = await fetch('/crumbapi/blockly-terminal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'blockly', blockcode: code, vars: knownVars })
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
out('\n✅ Antwort:\n' + JSON.stringify(data, null, 2));
} catch (err) {
// Fallback: Echo-JSON lokal anzeigen
const echo = {
ok: true,
mode: 'echo-fallback',
request: { mode: 'blockly', blockcode: code, vars: knownVars }
};
out('\n Endpoint nicht bereit. Fallback aktiv.\n\n' + JSON.stringify(echo, null, 2));
}
}
// Direkt beim Laden einmal die Demo hineinlegen:
loadDemo();
</script>
</div>
</body>
</html>

298
crumbblocks/index.html Normal file
View File

@@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Blockly to Crumbforest Terminal Bridge</title>
<!-- Blockly + JS Generator + Deutsche Labels -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root {
--bg: #1e1e1e;
--panel: #111;
--text: #e8e8e8;
--ok: #a6e22e;
--btn: #4caf50;
}
html, body { height: 100%; margin: 0; background: var(--bg); color: var(--text); font-family: monospace; }
header { display:flex; gap:.5rem; align-items:center; padding:.5rem .75rem; }
header h2 { margin:0; font-weight:600; }
header .spacer { flex:1; }
button {
padding:.6rem .9rem; background: var(--btn); color:#fff; border:none; border-radius:8px; font-weight:700; cursor:pointer;
}
#wrap { display:flex; flex-direction:column; height: calc(100vh - 56px); }
#blocklyDiv { height: 64vh; width: 100%; }
#bottom { display:flex; gap:.75rem; padding:.5rem .75rem; }
#output {
flex:1; height: 32vh; background:var(--panel); color:var(--ok);
padding:10px; overflow:auto; white-space: pre-wrap; word-break: break-word; border-radius:8px;
}
#controls { display:flex; flex-direction:column; gap:.5rem; }
.muted { opacity:.7; }
</style>
</head>
<body>
<header>
<h2>🧁 Blockly Crumbforest Bridge</h2>
<div class="spacer"></div>
<button id="btnDemo" title="Mini-Demo laden">🧪 Demo</button>
<button id="btnClear" title="Workspace leeren">🗑️ Neu</button>
<button id="btnZoomIn" title="Zoom +">🔍+</button>
<button id="btnZoomOut" title="Zoom ">🔍-</button>
<button id="btnRun" title="Code ausführen / an Terminal senden">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="blocklyDiv"></div>
<div id="bottom">
<div id="controls">
<div class="muted">Variablen: energie_wh, verbrauch_wh, sensor_temp_c, power_w, wasser_ml, zeit_s</div>
<div class="muted">Endpoint: <code>/crumbapi/blockly-terminal</code> (Echo-Fallback aktiv)</div>
</div>
<pre id="output">🌲 Terminal wartet ...</pre>
</div>
</div>
<!-- Toolbox: wie bei dir, nur Loops aktiviert -->
<xml id="toolbox" style="display:none">
<block type="text"></block>
<block type="text_print"></block>
<block type="controls_if"></block>
<block type="logic_compare"></block>
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="variables_set"></block>
<block type="variables_get"></block>
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="procedures_defnoreturn"></block>
<block type="procedures_callnoreturn"></block>
</xml>
<script>
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing: 24, length: 3, colour: '#484848', snap: true},
trashcan: true,
zoom: {startScale: 1.15, maxScale: 2.0, minScale: 0.6, controls: false, wheel: true},
move: {scrollbars: true, drag: true, wheel: true}
});
// Vordefinierte Variablen für euren „Prozess“-Wortschatz
['energie_wh','verbrauch_wh','sensor_temp_c','power_w','wasser_ml','zeit_s']
.forEach(v => { try { workspace.createVariable(v); } catch(e){} });
// Helpers
const $ = sel => document.querySelector(sel);
function setOutput(txt){ $('#output').textContent = txt; }
function appendOutput(txt){
const el = $('#output');
el.textContent += (el.textContent.endsWith('\n')?'':'\n') + txt;
el.scrollTop = el.scrollHeight;
}
// Initial-Variablen aus „set“-Blöcken sammeln (für JSON-Vorschau)
function collectInitialVars() {
const vals = {};
const blocks = workspace.getAllBlocks(false);
for (const b of blocks) {
if (b.type === 'variables_set') {
const name = b.getFieldValue('VAR');
// Versuche simples Literal zu lesen (Zahl/Text)
const input = b.getInputTargetBlock('VALUE');
if (input) {
if (input.type === 'math_number') {
vals[name] = Number(input.getFieldValue('NUM'));
} else if (input.type === 'text') {
vals[name] = input.getFieldValue('TEXT') ?? '';
} else {
// Irgendwas anderes verschachtelt → nur markieren
vals[name] = '<expr>';
}
} else {
vals[name] = '<unset>';
}
}
}
return vals;
}
// Kompatibles XML-Parserchen (fix für „Blockly.Xml.textToDom is not a function“)
function textToDom(xmlText){
if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText);
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
// Aktionen
$('#btnZoomIn').onclick = ()=> Blockly.svgResize(workspace), workspace.zoomCenter(1);
$('#btnZoomOut').onclick = ()=> Blockly.svgResize(workspace), workspace.zoomCenter(-1);
$('#btnClear').onclick = () => {
workspace.clear();
setOutput('🧹 Workspace geleert.');
};
$('#btnDemo').onclick = () => {
const demoXML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>sensor_temp_c</variable>
<variable>power_w</variable>
<variable>wasser_ml</variable>
<variable>zeit_s</variable>
<variable>energie_wh</variable>
<variable>verbrauch_wh</variable>
</variables>
<block type="variables_set" x="24" y="22">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE"><block type="math_number"><field name="NUM">20</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">power_w</field>
<value name="VALUE"><block type="math_number"><field name="NUM">2000</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">wasser_ml</field>
<value name="VALUE"><block type="math_number"><field name="NUM">250</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">zeit_s</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">energie_wh</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">verbrauch_wh</field>
<value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next>
<block type="controls_whileUntil">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare">
<field name="OP">LT</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">100</field></block></value>
</block>
</value>
<statement name="DO">
<block type="text_print">
<value name="TEXT">
<block type="text">
<field name="TEXT">Heize...</field>
</block>
</value>
<next>
<block type="variables_set">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">5</field></block></value>
</block>
</value>
<next>
<block type="variables_set">
<field name="VAR">zeit_s</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">zeit_s</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">5</field></block></value>
</block>
</value>
<next>
<block type="variables_set">
<field name="VAR">energie_wh</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">energie_wh</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">2.8</field></block></value>
</block>
</value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</statement>
<next>
<block type="text_print">
<value name="TEXT">
<block type="text">
<field name="TEXT">Fertig. STOP.</field>
</block>
</value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</xml>`;
try {
const dom = textToDom(demoXML);
workspace.clear();
Blockly.Xml.domToWorkspace(dom, workspace);
setOutput('✅ Demo geladen. Passe die Logik an und drücke ▶️ Ausführen.');
} catch (e) {
setOutput('❌ Demo konnte nicht geladen werden:\n' + e);
}
};
$('#btnRun').onclick = async () => {
const code = Blockly.JavaScript.workspaceToCode(workspace);
const initVars = collectInitialVars();
setOutput("📤 Sende an Terminal...\n\n" + code);
const payload = {
mode: "blockly",
blockcode: code,
preview_vars: initVars
};
try {
const res = await fetch("/crumbapi/blockly-terminal", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
let data;
if (res.ok) {
data = await res.json().catch(()=>({}));
appendOutput("\n\n✅ Antwort:\n" + JSON.stringify(data, null, 2));
} else {
appendOutput("\n\n Endpoint nicht bereit (HTTP " + res.status + "). Fallback aktiv.");
appendOutput("\n\n✅ Antwort (Fallback):\n" + JSON.stringify({ ok:true, mode:"echo-fallback", request:payload }, null, 2));
}
} catch (err) {
appendOutput("\n\n❌ Netzwerkfehler: " + err);
appendOutput("\n\n✅ Antwort (Fallback):\n" + JSON.stringify({ ok:true, mode:"echo-fallback", request:payload }, null, 2));
}
};
// Resizing
window.addEventListener('resize', () => Blockly.svgResize(workspace));
</script>
</body>
</html>

278
crumbblocks/index_v1.html Normal file
View File

@@ -0,0 +1,278 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Crumbblocks Blockly ↔ Crumbforest</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly + Python-Generator + Deutsch -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/python.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{
--bg:#0d0f12;--panel:#12151b;--ink:#e9edf5;--muted:#b7c0d0;--accent:#4caf50;--danger:#e74c3c;
--mono:ui-monospace, Menlo, Monaco, "Courier New", monospace;
}
html,body{height:100%;margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono)}
header{position:sticky;top:0;z-index:5;display:flex;gap:.6rem;align-items:center;padding:.6rem 1rem;background:#0b0d10cc;border-bottom:1px solid #1b212c}
header h2{margin:0;font-size:1rem;color:var(--muted)}
.spacer{flex:1 1 auto}
.btn{border:1px solid #263041;background:#161b22;color:var(--ink);border-radius:8px;padding:.5rem .75rem;cursor:pointer;font-weight:700}
.btn:hover{filter:brightness(1.1)}
.btn.primary{background:var(--accent);color:#0b130c;border-color:#2b6b2e}
.btn.danger{background:var(--danger);border-color:#b13a2e}
.btn.ghost{background:transparent}
#wrap{display:flex;flex-direction:column;gap:.6rem;height:calc(100% - 54px);padding:.6rem 1rem 1rem}
#blocklyDiv{height:62vh;width:100%;border-radius:10px;overflow:hidden;background:#0f1319;border:1px solid #1b212c}
#console{display:grid;grid-template-rows:auto 1fr;gap:.4rem;height:28vh;background:var(--panel);border:1px solid #1b212c;border-radius:10px}
#consoleHead{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;border-bottom:1px solid #1b212c}
#statusDot{width:.6rem;height:.6rem;border-radius:50%;background:#6a6f79;display:inline-block}
#statusText{color:var(--muted);font-size:.9rem}
#output{margin:0;padding:.6rem .8rem;overflow:auto;line-height:1.35;color:#a6ffad;background:#0c0f13;border-radius:0 0 10px 10px;white-space:pre-wrap;word-break:break-word;font-size:.93rem}
@media (max-width:960px){#blocklyDiv{height:56vh}#console{height:32vh}}
.blocklyMainBackground{fill:#0f1319 !important}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbblocks</h2>
<div class="spacer"></div>
<button class="btn" id="btnDemo">Demo laden</button>
<button class="btn ghost" id="btnZoomOut"></button>
<button class="btn ghost" id="btnZoomIn">+</button>
<button class="btn danger" id="btnTrash">Mülleimer</button>
<button class="btn primary" id="btnRun">Ausführen → Terminal</button>
</header>
<div id="wrap">
<div id="blocklyDiv" aria-label="Blockly Arbeitsfläche"></div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Logik" colour="#66b1ff">
<block type="controls_if"></block>
<block type="logic_compare"></block>
<block type="logic_operation"></block>
<block type="logic_boolean"></block>
</category>
<category name="Schleifen" colour="#6cdf6c">
<block type="controls_repeat_ext">
<value name="TIMES"><shadow type="math_number"><field name="NUM">10</field></shadow></value>
</block>
<block type="controls_whileUntil"></block>
</category>
<category name="Mathe" colour="#b68cff">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
</category>
<category name="Text" colour="#ffd866">
<block type="text"></block>
<block type="text_print"></block>
</category>
<sep gap="8"></sep>
<category name="Variablen" custom="VARIABLE" colour="#ff75b5"></category>
<category name="Funktionen" custom="PROCEDURE" colour="#e056fd"></category>
</xml>
<section id="console" aria-label="Ausgabe">
<div id="consoleHead">
<span id="statusDot" title="Status"></span>
<strong>Konsole</strong>
<span id="statusText">bereit</span>
<div class="spacer"></div>
<button class="btn ghost" id="btnClear">Leeren</button>
<button class="btn ghost" id="btnToggle">Ein-/Ausklappen</button>
</div>
<pre id="output">🌲 Terminal wartet …</pre>
</section>
</div>
<script>
// eigenes Dark-Theme
const CrumbDark = Blockly.Theme.defineTheme('CrumbDark', {
base: Blockly.Themes.Dark,
componentStyles: {
workspaceBackgroundColour: '#0f1319',
toolboxBackgroundColour: '#0e1217',
toolboxForegroundColour: '#e9edf5',
flyoutBackgroundColour: '#0f1319',
flyoutForegroundColour: '#e9edf5',
scrollbarColour: '#2a3240',
insertionMarkerColour: '#4caf50',
insertionMarkerOpacity: 0.35,
cursorColour: '#4caf50',
}
});
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: CrumbDark,
renderer: 'zelos',
trashcan: true,
grid: { spacing: 28, length: 2, colour: '#1a2029', snap: true },
move: { scrollbars: true, drag: true, wheel: true },
zoom: { controls: false, wheel: true, startScale: 1.15, maxScale: 2.2, minScale: 0.7, scaleSpeed: 1.2 },
});
window.addEventListener('resize', () => Blockly.svgResize(workspace));
// VORDEFINIERTE VARIABLEN für die Kids
const PRESET_VARS = [
'energie_wh','verbrauch_wh','sensor_temp_c','power_w','wasser_ml','zeit_s'
];
for (const name of PRESET_VARS) {
try { workspace.createVariable(name); } catch(_) {}
}
// UI helpers
const outputEl = document.getElementById('output');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const consoleBox = document.getElementById('console');
function setStatus(color, text){ statusDot.style.background=color; statusText.textContent=text||''; }
function log(line){ outputEl.textContent += (outputEl.textContent.endsWith('\n')?'':'\n')+ line; outputEl.scrollTop = outputEl.scrollHeight; }
function resetOutput(msg='🌲 Terminal wartet …'){ outputEl.textContent = msg; }
// Buttons
document.getElementById('btnZoomIn').onclick = () => workspace.zoomCenter(1);
document.getElementById('btnZoomOut').onclick = () => workspace.zoomCenter(-1);
document.getElementById('btnTrash').onclick = () => workspace.clear();
document.getElementById('btnClear').onclick = () => resetOutput('— leere Ausgabe —');
let collapsed=false;
document.getElementById('btnToggle').onclick = () => { collapsed=!collapsed; consoleBox.style.display = collapsed?'none':'grid'; };
// Demo-Workspace (Wasserkocher) via DOMParser, ohne Blockly.Xml.textToDom
const DEMO_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>energie_wh</variable>
<variable>verbrauch_wh</variable>
<variable>sensor_temp_c</variable>
<variable>power_w</variable>
<variable>wasser_ml</variable>
<variable>zeit_s</variable>
</variables>
<block type="variables_set" x="20" y="20">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE"><block type="math_number"><field name="NUM">20</field></block></value>
<next>
<block type="variables_set">
<field name="VAR">power_w</field>
<value name="VALUE"><block type="math_number"><field name="NUM">800</field></block></value>
<next>
<block type="controls_whileUntil">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare">
<field name="OP">LT</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">100</field></block></value>
</block>
</value>
<statement name="DO">
<block type="variables_set">
<field name="VAR">sensor_temp_c</field>
<value name="VALUE">
<block type="math_arithmetic">
<field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">10</field></block></value>
</block>
</value>
<next>
<block type="text_print">
<value name="TEXT">
<block type="text"><field name="TEXT">Erhitze… Temp=</field></block>
</value>
<next>
<block type="text_print">
<value name="TEXT"><block type="variables_get"><field name="VAR">sensor_temp_c</field></block></value>
</block>
</next>
</block>
</next>
</block>
</statement>
<next>
<block type="text_print">
<value name="TEXT"><block type="text"><field name="TEXT">Wasser kocht! 🔥</field></block></value>
</block>
</next>
</block>
</next>
</block>
</next>
</block>
</xml>`.trim();
function loadDemo(){
try{
workspace.clear();
const dom = new DOMParser().parseFromString(DEMO_XML, 'text/xml').documentElement;
// domToWorkspace ist in allen Builds vorhanden
Blockly.Xml.domToWorkspace(dom, workspace);
Blockly.svgResize(workspace);
setStatus('#4caf50','Demo geladen');
resetOutput('🧪 Demo geladen „Wasserkocher“ bereit.');
}catch(e){
setStatus('#e74c3c','Demo-Fehler');
resetOutput('❌ Demo konnte nicht geladen werden:\n' + (e?.message||e));
console.error(e);
}
}
document.getElementById('btnDemo').onclick = loadDemo;
// Run nur aktiv wenn es Blöcke gibt
const btnRun = document.getElementById('btnRun');
function updateRunDisabled(){
const disabled = workspace.getAllBlocks(false).length===0;
btnRun.disabled = disabled;
btnRun.style.opacity = disabled ? .55 : 1;
btnRun.style.cursor = disabled ? 'not-allowed' : 'pointer';
}
workspace.addChangeListener(updateRunDisabled);
updateRunDisabled();
// Ausführen → Bridge (mit Echo-Fallback)
async function runCode(){
const code = Blockly.Python.workspaceToCode(workspace);
resetOutput('📤 Sende an Terminal…\n\n' + code);
setStatus('#f1c40f','sende…');
const ctrl = new AbortController();
const timer = setTimeout(()=>ctrl.abort(), 10000);
async function post(url){
const res = await fetch(url, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ blockcode: code }),
signal: ctrl.signal
});
clearTimeout(timer);
if(!res.ok) throw new Error('HTTP '+res.status+' '+(await res.text()).slice(0,400));
return res.json().catch(()=>({}));
}
try{
let data;
try{
// deine echte Bridge
data = await post('/crumbapi/blockly-terminal');
}catch(_){
// Fallback-Echo (damit Kids sofort Feedback sehen)
data = { ok:true, mode:'echo-fallback', code:code };
}
setStatus('#4caf50','fertig');
log('\n✅ Antwort:\n' + JSON.stringify(data, null, 2));
}catch(err){
setStatus('#e74c3c','Fehler');
log('\n❌ Fehler beim Senden:\n' + (err?.message||err));
}
}
btnRun.addEventListener('click', runCode);
// Startzustand: Demo direkt laden
loadDemo();
</script>
</body>
</html>

View File

@@ -0,0 +1,575 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (SAFE)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{--bg:#1e1e1e;--panel:#121212;--txt:#e8e8e8;--accent:#4caf50;--muted:#a0a0a0;--line:#2a2a2a}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto}
.hint{color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (SAFE)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Reject bei Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear" class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg"></div>
<div class="hint">
Marker (6S): 19,2 V (leer, ~3,2/Z) • 22,2 V (Nominal) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
</div>
</div>
</div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// === CFG (Gedankenstütze) ===
const CFG = {
cells: 6,
v_full: 4.20, // Full
v_storage: 3.80, // Storage
v_hv: 4.35, // HV
v_min: 3.20, // "ehrlich leer"
drift_reject: 0.30, // harte Zellabweichung ablehnen
tick_s: 10,
dv_charge: 0.01, // V/Z pro Tick (vereinfacht)
dv_discharge: -0.01,
capacity_mAh: 2200,
c_rate: 1.0
};
const cfgEl = document.getElementById('cfg');
cfgEl.textContent = 'CFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
// === Blockly Setup ===
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Vordefinierte Variablen
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// Output Helpers
const $ = s => document.querySelector(s);
function setOutput(t){ $('#output').textContent = t }
function appendOutput(t){
const el = $('#output');
el.textContent += (el.textContent.endsWith('\n')?'':'\n') + t;
el.scrollTop = el.scrollHeight;
}
window.appendOutput = appendOutput;
// === Patch 1: text_print -> Panel & Alerts abfangen ===
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
})();
// XML util
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// === DOC XML ===
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z → 25,2V • Storage 3,80V/Z → 22,8V • HV 4,35V/Z → 26,1V • Ende ~3,20V/Z → 19,2V.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance). Packs mit harten Unterschieden nicht nutzen.</field></block></value></block>
</xml>
`.trim();
// === T1: Storage (3.80V/Z) Entladen bis Ziel, mit Clamp ===
const T1_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable><variable>i</variable><variable>new_v</variable>
</variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value>
<next><block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value>
</next></next></next></next>
</block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field>
<value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value>
</block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block>
</value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value>
</block></value>
<next>
<block type="controls_forEach">
<field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">0</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">5</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next>
<block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO0">
<block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block>
</statement>
</block>
</next>
<next>
<block type="lists_setIndex">
<mutation at="true"></mutation>
<field name="MODE">SET</field>
<field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block>
</next>
</statement>
</block>
</next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800">
<value name="TEXT">
<block type="text_join">
<mutation items="8"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* siehe 'cells' */]" }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === T2: Full (4.20V/Z) Laden bis Ziel, mit Clamp ===
const T2_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable><variable>i</variable><variable>new_v</variable>
</variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">charge</field></block></value>
<next><block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.2</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value>
</next></next></next></next>
</block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field>
<value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.70</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.68</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.72</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.69</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.71</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.67</field></block></value>
</block></value>
</block>
<block type="variables_set" x="20" y="280"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="320">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block>
</value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value>
</block></value>
<next>
<block type="controls_forEach">
<field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">0</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">5</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next>
<block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO0">
<block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block>
</statement>
</block>
</next>
<next>
<block type="lists_setIndex">
<mutation at="true"></mutation>
<field name="MODE">SET</field>
<field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block>
</next>
</statement>
</block>
</next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">full_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="790">
<value name="TEXT">
<block type="text_join">
<mutation items="8"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"charge", "target_cell_v":4.2, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* siehe 'cells' */]" }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === T3: Reject bei Drift >= 0.30V ===
const T3_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>stop_reason</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">safety_check</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.60</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.61</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.58</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.59</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.05</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.62</field></block></value>
</block></value>
<block type="variables_set" x="20" y="200"><field name="VAR">max_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<next><block type="variables_set"><field name="VAR">min_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value></block></next>
</block>
<block type="controls_if" x="20" y="270">
<value name="IF0"><block type="logic_compare"><field name="OP">GTE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="B"><block type="math_number"><field name="NUM">0.3</field></block></value>
</block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">reject_imbalance</field></block></value></block></statement>
</block>
<block type="text_print" x="20" y="350">
<value name="TEXT">
<block type="text_join">
<mutation items="10"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode": "safety_check", "max_v": </field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">max_v</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "min_v": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">min_v</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "delta": </field></block></value>
<value name="ADD5"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="ADD6"><block type="text"><field name="TEXT">, "stop_reason": "</field></block></value>
<value name="ADD7"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD8"><block type="text"><field name="TEXT">", "advice": "Pack nicht verwenden."</field></block></value>
<value name="ADD9"><block type="text"><field name="TEXT"> }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === T4: HV 4.35V/Z Laden bis Ziel, mit Clamp ===
const T4_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable>
</variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">hv_charge</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.35</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
<block type="variables_set" x="20" y="140"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value></block>
<block type="variables_set" x="20" y="180"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></block>
<block type="variables_set" x="20" y="220"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.95</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.96</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.97</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.94</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.98</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.96</field></block></value>
</block></value>
<block type="variables_set" x="20" y="360"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="400">
<field name="MODE">WHILE</field>
<value name="BOOL">
<block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block>
</value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value>
</block></value>
<next>
<block type="controls_forEach">
<field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">0</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">5</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next>
<block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block>
</next>
<next>
<block type="lists_setIndex">
<mutation at="true"></mutation>
<field name="MODE">SET</field>
<field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block>
</next>
</statement>
</block>
</next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">hv_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="820">
<value name="TEXT">
<block type="text_join">
<mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"hv_charge", "target_cell_v":4.35, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "cells_v":"[/* 'cells' */]" }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === Zufalls-Startwerte (nur Inspektion) ===
function randomDemoXml(){
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
return `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>
`.trim();
}
// === Buttons ===
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Reject bei Drift ≥ 0,30V. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z (nur Demo). ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{ loadXml(randomDemoXml()); setOutput('🎲 Zufallspack geladen.'); };
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// === Patch 2: Watchdog im Run-Handler ===
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
code = guardHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
</script>
<!-- Sicherheit (sichtbar für Kids/Trainer) -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
Packs mit starker Zellabweichung (<i>≥0,30 V</i>) nicht verwenden. Keine echten Ströme werden hier geschaltet.
</div>
</body>
</html>

View File

@@ -0,0 +1,453 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (SAFE v3)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{--bg:#1e1e1e;--panel:#121212;--txt:#e8e8e8;--accent:#4caf50;--muted:#a0a0a0;--line:#2a2a2a}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto}
.hint{color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (SAFE v3)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear" class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg"></div>
<div class="hint">
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
</div>
</div>
</div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// === CFG (Gedankenstütze) ===
const CFG = {
cells: 6,
v_full: 4.20,
v_storage: 3.80,
v_hv: 4.35,
v_min: 3.20,
drift_reject: 0.30,
tick_s: 10,
dv_charge: 0.01,
dv_discharge: -0.01,
capacity_mAh: 2200,
c_rate: 1.0
};
const cfgEl = document.getElementById('cfg');
cfgEl.textContent = 'CFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
// === Blockly Setup ===
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Variablen, die in den Demos genutzt werden
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// Output/Print
const $ = s => document.querySelector(s);
function setOutput(t){ $('#output').textContent = t }
function appendOutput(t){ const el=$('#output'); el.textContent += (el.textContent.endsWith('\n')?'':'\n') + t; el.scrollTop = el.scrollHeight; }
window.appendOutput = appendOutput;
// text_print → Panel & Alerts abfangen
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
})();
// *** LISTEN-PATCH (1-basiert → korrektes JS-Indexing & Schreiben) ***
(function patchListsGenerators(){
const gen = Blockly.JavaScript;
gen['lists_getIndex'] = function(block){
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const code = `${list}[(${at}) - 1]`;
return [code, gen.ORDER_MEMBER];
};
gen['lists_setIndex'] = function(block){
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
return `${list}[(${at}) - 1] = ${to};\n`;
};
})();
// XML utils
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// --- Demos ---
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
</xml>
`.trim();
const T1_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
const T2_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">charge</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.2</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.70</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.68</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.72</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.69</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.71</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.67</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="280"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="320"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">full_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="790"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"charge", "target_cell_v":4.2, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
const T3_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>stop_reason</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">safety_check</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.60</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.61</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.58</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.59</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.05</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.62</field></block></value>
</block></value>
<block type="variables_set" x="20" y="200"><field name="VAR">max_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<next><block type="variables_set"><field name="VAR">min_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value></block></next>
</block>
<block type="controls_if" x="20" y="270"><value name="IF0"><block type="logic_compare"><field name="OP">GTE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="B"><block type="math_number"><field name="NUM">0.3</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">reject_imbalance</field></block></value></block></statement>
</block>
<block type="text_print" x="20" y="350"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"safety_check", "delta": </field></block></value>
<value name="ADD1"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "stop_reason":"</field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">", "advice":"Pack nicht verwenden." }</field></block></value>
</block></value></block>
</xml>
`.trim();
const T4_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">hv_charge</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.35</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
<block type="variables_set" x="20" y="140"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value></block>
<block type="variables_set" x="20" y="180"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></block>
<block type="variables_set" x="20" y="220"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.95</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.96</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.97</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.94</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.98</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.96</field></block></value>
</block></value>
<block type="variables_set" x="20" y="360"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="400"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">hv_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="820"><value name="TEXT"><block type="text_join"><mutation items="4"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"hv_charge", "target_cell_v":4.35, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
function randomDemoXml(){
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
return `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>
`.trim();
}
// Buttons
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z (nur Demo). ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{ loadXml(randomDemoXml()); setOutput('🎲 Zufallspack geladen.'); };
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// RUN: Watchdog + Alerts→Panel
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
code = guardHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
</script>
<!-- Sicherheit -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
Packs mit starker Zellabweichung (≥0,30 V) nicht verwenden. Keine echten Ströme werden hier geschaltet.
</div>
</body>
</html>

View File

@@ -0,0 +1,322 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (SAFE v4)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{--bg:#1e1e1e;--panel:#121212;--txt:#e8e8e8;--accent:#4caf50;--muted:#a0a0a0;--line:#2a2a2a}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto}
.hint{color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (SAFE v4)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear" class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg"></div>
<div class="hint">
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
</div>
</div>
</div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// === CFG (Gedankenstütze) ===
const CFG = {
cells: 6,
v_full: 4.20,
v_storage: 3.80,
v_hv: 4.35,
v_min: 3.20,
drift_reject: 0.30,
tick_s: 10,
dv_charge: 0.01,
dv_discharge: -0.01,
capacity_mAh: 2200,
c_rate: 1.0
};
const cfgEl = document.getElementById('cfg');
cfgEl.textContent = 'CFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
// === Blockly Setup ===
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Variablen, die in den Demos genutzt werden
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// Output/Print
const $ = s => document.querySelector(s);
function setOutput(t){ $('#output').textContent = t }
function appendOutput(t){ const el=$('#output'); el.textContent += (el.textContent.endsWith('\n')?'':'\n') + t; el.scrollTop = el.scrollHeight; }
window.appendOutput = appendOutput;
// text_print → Panel & Alerts abfangen
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
})();
// *** LISTEN-PATCH (1-basiert → korrektes JS-Indexing & Schreiben) ***
(function patchListsGenerators(){
const gen = Blockly.JavaScript;
gen['lists_getIndex'] = function(block){
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const code = `${list}[(${at}) - 1]`;
return [code, gen.ORDER_MEMBER];
};
gen['lists_setIndex'] = function(block){
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
return `${list}[(${at}) - 1] = ${to};\n`;
};
})();
// XML utils
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// --- Demos (kompakt gehalten) ---
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
</xml>`.trim();
const T1_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>`.trim();
const T2_XML = /* wie T1, nur 4.2V + dv=+0.01 + MIN/Clamp im If */;
const T3_XML = /* Drift-Check */;
const T4_XML = /* HV 4.35V */;
// Buttons
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z (nur Demo). ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
const XML = `<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>`;
loadXml(XML);
setOutput('🎲 Zufallspack geladen.');
};
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// === RUN: Watchdog + Alerts→Panel + Writeback&Clamp-Fix ===
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
const clampHeader = `
function __clamp(v, t, mode){
if(mode==='charge' || mode==='hv_charge') return Math.min(v, t);
if(mode==='storage') return Math.max(v, t);
return v;
}
`;
// 1) Watchdog in jede while(...)
code = guardHeader + clampHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
// 2) Alerts → Panel
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
// 3) Erzwinge Schreibvorgang + Clamp nach "new_v = cells[...] + dv;"
// - Variante mit 1-basiertem Index: cells[(i - 1)]
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*\(\s*i\s*(?:-\s*1)?\s*\)\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[(i - 1)] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[(i - 1)] = new_v;"
);
// - Variante mit 0-basiertem Index: cells[i]
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*i\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[i] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[i] = new_v;"
);
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
</script>
<!-- Sicherheit -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
Packs mit starker Zellabweichung (≥0,30 V) nicht verwenden. Keine echten Ströme werden hier geschaltet.
</div>
</body>
</html>

View File

@@ -0,0 +1,322 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (SAFE v4)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root{--bg:#1e1e1e;--panel:#121212;--txt:#e8e8e8;--accent:#4caf50;--muted:#a0a0a0;--line:#2a2a2a}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto}
.hint{color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (SAFE v4)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear" class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg"></div>
<div class="hint">
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
</div>
</div>
</div>
<!-- Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// === CFG (Gedankenstütze) ===
const CFG = {
cells: 6,
v_full: 4.20,
v_storage: 3.80,
v_hv: 4.35,
v_min: 3.20,
drift_reject: 0.30,
tick_s: 10,
dv_charge: 0.01,
dv_discharge: -0.01,
capacity_mAh: 2200,
c_rate: 1.0
};
const cfgEl = document.getElementById('cfg');
cfgEl.textContent = 'CFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
// === Blockly Setup ===
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Variablen, die in den Demos genutzt werden
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// Output/Print
const $ = s => document.querySelector(s);
function setOutput(t){ $('#output').textContent = t }
function appendOutput(t){ const el=$('#output'); el.textContent += (el.textContent.endsWith('\n')?'':'\n') + t; el.scrollTop = el.scrollHeight; }
window.appendOutput = appendOutput;
// text_print → Panel & Alerts abfangen
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
})();
// *** LISTEN-PATCH (1-basiert → korrektes JS-Indexing & Schreiben) ***
(function patchListsGenerators(){
const gen = Blockly.JavaScript;
gen['lists_getIndex'] = function(block){
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const code = `${list}[(${at}) - 1]`;
return [code, gen.ORDER_MEMBER];
};
gen['lists_setIndex'] = function(block){
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
return `${list}[(${at}) - 1] = ${to};\n`;
};
})();
// XML utils
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// --- Demos (kompakt gehalten) ---
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
</xml>`.trim();
const T1_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>`.trim();
const T2_XML = /* wie T1, nur 4.2V + dv=+0.01 + MIN/Clamp im If */;
const T3_XML = /* Drift-Check */;
const T4_XML = /* HV 4.35V */;
// Buttons
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z (nur Demo). ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
const XML = `<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>`;
loadXml(XML);
setOutput('🎲 Zufallspack geladen.');
};
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// === RUN: Watchdog + Alerts→Panel + Writeback&Clamp-Fix ===
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
const clampHeader = `
function __clamp(v, t, mode){
if(mode==='charge' || mode==='hv_charge') return Math.min(v, t);
if(mode==='storage') return Math.max(v, t);
return v;
}
`;
// 1) Watchdog in jede while(...)
code = guardHeader + clampHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
// 2) Alerts → Panel
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
// 3) Erzwinge Schreibvorgang + Clamp nach "new_v = cells[...] + dv;"
// - Variante mit 1-basiertem Index: cells[(i - 1)]
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*\(\s*i\s*(?:-\s*1)?\s*\)\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[(i - 1)] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[(i - 1)] = new_v;"
);
// - Variante mit 0-basiertem Index: cells[i]
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*i\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[i] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[i] = new_v;"
);
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
</script>
<!-- Sicherheit -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
Packs mit starker Zellabweichung (≥0,30 V) nicht verwenden. Keine echten Ströme werden hier geschaltet.
</div>
</body>
</html>

View File

@@ -0,0 +1,409 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (DIAG v6)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root{--bg:#0f1115;--panel:#11151a;--txt:#e6edf3;--accent:#4caf50;--muted:#9aa4b2;--line:#21262d;--bad:#ff4d4f}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%;border:1px solid var(--line);border-radius:10px}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto;border:1px solid var(--line)}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto;border:1px solid var(--line)}
.hint{color:var(--muted);font-size:.9rem}
.fail{color:var(--bad);font-weight:700}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (DIAG v6)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear"class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg">Diag lädt …</div>
<div class="hint">
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
</div>
</div>
</div>
<!-- TOOLBOX (wird erst genutzt, wenn Blockly geladen ist) -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// ---------- Diagnose: Fehler ins Panel ----------
const out = document.getElementById('output');
const cfgEl = document.getElementById('cfg');
function setOutput(t){ out.textContent = t }
function appendOutput(t){ out.textContent += (out.textContent.endsWith('\n')?'':'\n') + t; out.scrollTop = out.scrollHeight; }
window.appendOutput = appendOutput;
window.addEventListener('error', (e)=> appendOutput('❌ JS-Error: ' + e.message));
window.addEventListener('unhandledrejection', (e)=> appendOutput('❌ Promise-Rejection: ' + (e.reason && e.reason.message ? e.reason.message : e.reason)));
// ---------- Loader: erst lokal, dann CDN ----------
const CDN = 'https://unpkg.com';
const LOAD = [
['/crumbblocks/vendor/blockly/blockly.min.js', `${CDN}/blockly/blockly.min.js`],
['/crumbblocks/vendor/blockly/javascript.min.js', `${CDN}/blockly/javascript.min.js`],
['/crumbblocks/vendor/blockly/msg/de.js', `${CDN}/blockly/msg/de.js`],
];
function loadScriptPair([localSrc, cdnSrc]){
return new Promise((resolve, reject)=>{
const s = document.createElement('script');
s.src = localSrc;
s.onload = ()=> resolve({src:localSrc, ok:true, via:'local'});
s.onerror = ()=>{
const c = document.createElement('script');
c.src = cdnSrc;
c.onload = ()=> resolve({src:cdnSrc, ok:true, via:'cdn'});
c.onerror = ()=> reject(new Error('Laden fehlgeschlagen: ' + localSrc + ' und ' + cdnSrc));
document.head.appendChild(c);
};
document.head.appendChild(s);
});
}
// ---------- Self-Test ----------
async function selfTest(){
const diag = { js_ok:true, blockly_loaded:false, eval_ok:null, scripts:[], csp_note:null };
// Lade Reihenfolge wahren
for (const p of LOAD){
try {
const res = await loadScriptPair(p);
diag.scripts.push(res);
} catch (e) {
diag.scripts.push({src:p, ok:false, via:'none', err:String(e)});
}
}
diag.blockly_loaded = !!window.Blockly;
try { new Function('return 42')(); diag.eval_ok = true; }
catch(e){ diag.eval_ok = 'blocked: ' + e.message; diag.csp_note = 'CSP erlaubt kein unsafe-eval. Siehe Hinweise unten.'; }
const CFG = {
cells: 6, v_full:4.20, v_storage:3.80, v_hv:4.35, v_min:3.20,
drift_reject:0.30, tick_s:10, dv_charge:0.01, dv_discharge:-0.01,
capacity_mAh:2200, c_rate:1.0
};
cfgEl.textContent = 'Diag:\n' + JSON.stringify(diag, null, 2) + '\n\nCFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
if (!diag.blockly_loaded){
appendOutput('❌ Blockly nicht geladen. Prüfe /crumbblocks/vendor/... oder Internet/CSP.');
appendOutput('Tipp: Siehe „Lokale Vendor-Dateien“ unten.');
return;
}
bootBlockly(); // erst jetzt initialisieren
}
// ---------- Blockly-Init (nach bestandenem Self-Test) ----------
function bootBlockly(){
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Variablen
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// print ins Panel + Alert abfangen
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
})();
// 1-basige Listen sauber generieren
(function patchListsGenerators(){
const gen = Blockly.JavaScript;
gen['lists_getIndex'] = function(block){
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const code = `${list}[(${at}) - 1]`;
return [code, gen.ORDER_MEMBER];
};
gen['lists_setIndex'] = function(block){
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
return `${list}[(${at}) - 1] = ${to};\n`;
};
})();
// Helpers
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// Demos (kompakt)
const DOC_XML = `<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable></variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
</xml>`.trim();
const T1_XML = `<!-- Storage -->
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>`.trim();
const T2_XML = T1_XML
.replace('storage','charge')
.replace('3.8','4.2')
.replace('"-0.01"','"0.01"')
.replace('storage_reached','full_reached')
.replace('"GT"','"LT"');
const T3_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>stop_reason</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">safety_check</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.60</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.61</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.58</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.59</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.05</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.62</field></block></value>
</block></value>
<block type="variables_set" x="20" y="200"><field name="VAR">max_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<next><block type="variables_set"><field name="VAR">min_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value></block></next>
</block>
<block type="controls_if" x="20" y="270"><value name="IF0"><block type="logic_compare"><field name="OP">GTE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="B"><block type="math_number"><field name="NUM">0.3</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">reject_imbalance</field></block></value></block></statement>
</block>
<block type="text_print" x="20" y="350"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"safety_check", "delta": </field></block></value>
<value name="ADD1"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "stop_reason":"</field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">", "advice":"Pack nicht verwenden." }</field></block></value>
</block></value></block>
</xml>`.trim();
const T4_XML = T2_XML
.replace('charge','hv_charge')
.replace('4.2','4.35')
.replace('full_reached','hv_reached')
.replace(/3\.7[0-9]?/g,'3.96'); // Start näher an HV
// Buttons
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z. ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
const XML = `<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>`;
loadXml(XML);
setOutput('🎲 Zufallspack geladen.');
};
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// RUN: Watchdog + Alerts→Panel + Writeback&Clamp-Fix
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
const clampHeader = `
function __clamp(v, t, mode){
if(mode==='charge' || mode==='hv_charge') return Math.min(v, t);
if(mode==='storage') return Math.max(v, t);
return v;
}
`;
code = guardHeader + clampHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*\(\s*i\s*(?:-\s*1)?\s*\)\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[(i - 1)] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[(i - 1)] = new_v;"
);
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*i\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[i] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[i] = new_v;"
);
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
}
// Start
selfTest();
</script>
<!-- Sicherheit -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
Packs mit starker Zellabweichung (≥0,30 V) nicht verwenden. Keine echten Ströme werden hier geschaltet.
</div>
<!-- Hinweise / Fallback -->
<div style="padding:.6rem .8rem;color:var(--muted)">
<b>Lokale Vendor-Dateien (empfohlen, falls CDN/CSP blockt):</b><br/>
<code>mkdir -p /var/www/crumbblocks/vendor/blockly</code><br/>
<code>curl -L -o /var/www/crumbblocks/vendor/blockly/blockly.min.js https://unpkg.com/blockly/blockly.min.js</code><br/>
<code>curl -L -o /var/www/crumbblocks/vendor/blockly/javascript.min.js https://unpkg.com/blockly/javascript.min.js</code><br/>
<code>curl -L -o /var/www/crumbblocks/vendor/blockly/msg/de.js https://unpkg.com/blockly/msg/de.js</code><br/><br/>
<b>CSP (falls Eval/Inline blockiert → „nix passiert“):</b><br/>
Erweitere Header minimal für die Simulation:
<pre>Content-Security-Policy: default-src 'self';
script-src 'self' https://unpkg.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
connect-src 'self';
img-src 'self' data:;
font-src 'self' data:;</pre>
(Nur für diese Lernseite. Produktion später strenger machen.)
</div>
</body>
</html>

View File

@@ -0,0 +1,500 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🔋 6S LiPo Charger Blockly Simulation (DIAG v7)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root{--bg:#0f1115;--panel:#11151a;--txt:#e6edf3;--accent:#4caf50;--muted:#9aa4b2;--line:#21262d;--bad:#ff4d4f}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
header h2{margin:0;font-size:1rem;font-weight:700}
header .sp{flex:1}
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
#blocklyDiv{height:60vh;width:100%;border:1px solid var(--line);border-radius:10px}
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto;border:1px solid var(--line)}
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto;border:1px solid var(--line)}
.hint{color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • 6S LiPo Charger Simulation (DIAG v7)</h2>
<div class="sp"></div>
<button id="btnDoc" class="btn-gray">📄 Doku</button>
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
<button id="btnClear"class="btn-gray">🗑️ Neu</button>
<button id="btnRun">▶️ Ausführen</button>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg">Diag lädt …</div>
<div class="hint">
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z). Nur Simulation.
</div>
</div>
</div>
<!-- TOOLBOX -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_whileUntil"></block>
<block type="controls_forEach"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
<block type="math_on_list"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
<block type="lists_getIndex"></block>
<block type="lists_setIndex"></block>
<block type="lists_length"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
</xml>
<script>
// ---------- Diagnose & Output ----------
const out = document.getElementById('output');
const cfgEl = document.getElementById('cfg');
function setOutput(t){ out.textContent = t }
function appendOutput(t){ out.textContent += (out.textContent.endsWith('\n')?'':'\n') + t; out.scrollTop = out.scrollHeight; }
window.appendOutput = appendOutput;
window.addEventListener('error', e => appendOutput('❌ JS-Error: ' + e.message));
window.addEventListener('unhandledrejection', e => appendOutput('❌ Promise: ' + (e.reason && e.reason.message ? e.reason.message : e.reason)));
// ---------- Loader: erst lokal, dann CDN (klassische, nicht-ESM Bundles) ----------
const CDN = 'https://unpkg.com';
const LOAD = [
['/crumbblocks/vendor/blockly/blockly_compressed.js', `${CDN}/blockly/blockly_compressed.js`],
['/crumbblocks/vendor/blockly/blocks_compressed.js', `${CDN}/blockly/blocks_compressed.js`],
['/crumbblocks/vendor/blockly/javascript_compressed.js', `${CDN}/blockly/javascript_compressed.js`],
['/crumbblocks/vendor/blockly/msg/de.js', `${CDN}/blockly/msg/de.js`],
];
function loadScriptPair([localSrc, cdnSrc]){
return new Promise((resolve, reject)=>{
const s = document.createElement('script');
s.src = localSrc;
s.onload = ()=> resolve({src:localSrc, ok:true, via:'local'});
s.onerror = ()=>{
const c = document.createElement('script');
c.src = cdnSrc;
c.onload = ()=> resolve({src:cdnSrc, ok:true, via:'cdn'});
c.onerror = ()=> reject(new Error('Laden fehlgeschlagen: ' + localSrc + ' und ' + cdnSrc));
document.head.appendChild(c);
};
document.head.appendChild(s);
});
}
async function selfTest(){
const diag = { blockly_loaded:false, generator_loaded:false, eval_ok:null, scripts:[], csp_note:null };
for (const p of LOAD){
try { diag.scripts.push(await loadScriptPair(p)); }
catch(e){ diag.scripts.push({src:p, ok:false, via:'none', err:String(e)}); }
}
diag.blockly_loaded = !!window.Blockly;
diag.generator_loaded = !!(window.Blockly && Blockly.JavaScript);
try { new Function('return 42')(); diag.eval_ok = true; }
catch(e){ diag.eval_ok = 'blocked: ' + e.message; diag.csp_note = 'CSP blockiert eval/new Function.'; }
const CFG = { cells:6, v_full:4.20, v_storage:3.80, v_hv:4.35, v_min:3.20, drift_reject:0.30, tick_s:10, dv_charge:0.01, dv_discharge:-0.01, capacity_mAh:2200, c_rate:1.0 };
cfgEl.textContent = 'Diag:\n' + JSON.stringify(diag, null, 2) + '\n\nCFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
if (!diag.blockly_loaded || !diag.generator_loaded){ appendOutput('❌ Blockly/Gernerator fehlt. Prüfe Loader/CDN/CSP.'); return; }
bootBlockly();
}
// ---------- Blockly-Init ----------
function bootBlockly(){
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: {spacing:24, length:3, colour:'#474747', snap:true},
trashcan: true,
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
move: {scrollbars:true, drag:true, wheel:true}
});
// Lern-Variablen
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
// print → Panel
(function hardenPrint(){
const gen = Blockly.JavaScript;
gen['text_print'] = function(block){
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
return 'appendOutput(String(' + arg0 + '));\n';
};
const origAlert = window.alert;
window.alert = function(msg){ try { appendOutput('⚠️ alert: ' + msg); } catch(_) { origAlert(msg); } };
})();
// 1-basiert Listen → richtig lesen/schreiben
(function patchListsGenerators(){
const gen = Blockly.JavaScript;
gen['lists_getIndex'] = function(block){
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const code = `${list}[(${at}) - 1]`;
return [code, gen.ORDER_MEMBER];
};
gen['lists_setIndex'] = function(block){
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
return `${list}[(${at}) - 1] = ${to};\n`;
};
})();
// XML utils
function textToDom(xmlText){
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// --- Demos ---
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger Logik-Simulation (kein echtes Laden!).</field></block></value></block>
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
</block></value>
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
</block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
</next></next>
</block>
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
</xml>
`.trim();
// T1: Storage 3.8V/Z
const T1_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</block>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
// T2: Full 4.2V/Z (dv +, MIN<target)
const T2_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">charge</field></block></value><next>
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.2</field></block></value><next>
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.70</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.68</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.72</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.69</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.71</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.67</field></block></value>
</block></value></block>
<block type="variables_set" x="20" y="280"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="320"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">full_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="790"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"charge", "target_cell_v":4.2, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
// T3: Drift-Check
const T3_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>stop_reason</variable><variable>cells</variable><variable>max_v</variable><variable>min_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">safety_check</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.60</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.61</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.58</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.59</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.05</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.62</field></block></value>
</block></value>
<block type="variables_set" x="20" y="200"><field name="VAR">max_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<next><block type="variables_set"><field name="VAR">min_v</field><value name="VALUE"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value></block></next>
</block>
<block type="controls_if" x="20" y="270"><value name="IF0"><block type="logic_compare"><field name="OP">GTE</field>
<value name="A"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="B"><block type="math_number"><field name="NUM">0.3</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">reject_imbalance</field></block></value></block></statement>
</block>
<block type="text_print" x="20" y="350"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"safety_check", "delta": </field></block></value>
<value name="ADD1"><block type="math_arithmetic"><field name="OP">MINUS"><value name="A"><block type="variables_get"><field name="VAR">max_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">min_v</field></block></value></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "stop_reason":"</field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">", "advice":"Pack nicht verwenden." }</field></block></value>
</block></value></block>
</xml>
`.trim();
// T4: HV 4.35V/Z (dv +, MIN<target)
const T4_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">hv_charge</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">4.35</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
<block type="variables_set" x="20" y="140"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value></block>
<block type="variables_set" x="20" y="180"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></block>
<block type="variables_set" x="20" y="220"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">3.95</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">3.96</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3.97</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">3.94</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">3.98</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">3.96</field></block></value>
</block></value>
<block type="variables_set" x="20" y="360"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">0.01</field></block></value></block>
<block type="controls_whileUntil" x="20" y="400"><field name="MODE">WHILE</field>
<value name="BOOL"><block type="logic_compare"><field name="OP">LT</field>
<value name="A"><block type="math_on_list"><mutation op="MIN"></mutation><field name="OP">MIN</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
<next><block type="controls_forEach"><field name="VAR">i</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">new_v</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
</block></value>
</block>
<next><block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
</block></next>
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
</block></next>
</statement>
</block></next>
</statement>
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">hv_reached</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="820"><value name="TEXT"><block type="text_join"><mutation items="4"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"hv_charge", "target_cell_v":4.35, "stop_reason":"</field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">", "cells_v":"[/* 'cells' */]" }</field></block></value>
</block></value></block>
</xml>
`.trim();
// Buttons
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z. ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = ()=>{
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
const XML = `<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
</xml>`;
loadXml(XML);
setOutput('🎲 Zufallspack geladen.');
};
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
// RUN: Watchdog + Alerts→Panel + Writeback & Clamp
document.getElementById('btnRun').onclick = () => {
let code = Blockly.JavaScript.workspaceToCode(workspace);
const guardHeader = `
let __wf = 0, __wf_cap = 5000;
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
`;
const clampHeader = `
function __clamp(v, t, mode){
if(mode==='charge' || mode==='hv_charge') return Math.min(v, t);
if(mode==='storage') return Math.max(v, t);
return v;
}
`;
// Watchdog in jede while
code = guardHeader + clampHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
// Alerts → Panel
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
// Writeback & Clamp
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*\(\s*i\s*(?:-\s*1)?\s*\)\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[(i - 1)] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[(i - 1)] = new_v;"
);
code = code.replace(
/new_v\s*=\s*cells\s*\[\s*i\s*\]\s*\+\s*dv\s*;/g,
"new_v = __clamp(cells[i] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[i] = new_v;"
);
setOutput('▶️ Ausführung…\n\n' + code);
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
};
}
// Start
selfTest();
</script>
<!-- Sicherheit -->
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
⚠️ <b>Sicherheits-Hinweis:</b> Nur Simulation. Echte LiPos ausschließlich mit geeignetem Ladegerät & Balancer laden. Packs mit Zell-Delta ≥ 0,30 V nicht verwenden.
</div>
</body>
</html>

View File

@@ -0,0 +1,22 @@
// Minimal WebSocket echo server for testing Painter stream
// Usage: npm i ws; node mock_ws_server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 9980 });
console.log('WS server on ws://localhost:9980');
wss.on('connection', (ws)=>{
console.log('client connected');
ws.on('message', (msg)=>{
try{
const obj = JSON.parse(msg);
// log only essentials to keep console tidy
if(obj.type==='point'){
process.stdout.write(`• point ${obj.p.x.toFixed(1)},${obj.p.y.toFixed(1)}\r`);
}else{
console.log(obj.type);
}
}catch(e){}
// optionally echo back
// ws.send(msg);
});
ws.on('close', ()=>console.log('client closed'));
});

395
crumbblocks/painter.html Normal file
View File

@@ -0,0 +1,395 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>OneLiner Painter (MultiStroke) draw → export SVG + Motion JSON</title>
<style>
:root{
--bg: #0b0b10; --panel: #12121a; --muted:#8d93a1; --accent:#62d3a4; --accent2:#ffd166; --danger:#ff5c5c;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;display:grid;grid-template-rows:auto 1fr auto;background:var(--bg);color:#e7eaf0;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
header,footer{padding:12px 16px;background:var(--panel);border-bottom:1px solid #1e2230}
footer{border-top:1px solid #1e2230;border-bottom:none}
h1{font-size:1.05rem;margin:0}
.wrap{display:grid;grid-template-columns:340px 1fr;gap:16px;padding:16px}
@media (max-width:900px){.wrap{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid #1e2230;border-radius:12px;padding:14px;display:grid;gap:12px}
label{font-size:.9rem;color:#cfd5e3}
input[type="text"],textarea,select{width:100%;padding:10px;border-radius:10px;border:1px solid #2a3042;background:#0f121a;color:#e7eaf0}
input[type="color"]{width:48px;height:36px;border:none;background:none}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.btn{appearance:none;border:none;border-radius:10px;padding:10px 12px;background:#1f2636;color:#e7eaf0;cursor:pointer}
.btn:hover{background:#28314a}
.btn.accent{background:var(--accent);color:#0f131a;font-weight:600}
.btn.warn{background:var(--danger);color:#0f0f10}
.muted{color:var(--muted);font-size:.85rem}
.canvasWrap{position:relative;background:#0f121a;border:1px solid #1e2230;border-radius:12px;overflow:hidden}
svg{display:block;width:100%;height:100%;background:#0f121a}
.kbd{padding:1px 6px;border-radius:8px;background:#1e2333;color:#cfd5e3;font-family:ui-monospace,Menlo,Monaco,monospace}
ul#strokeList{list-style:none;margin:0;padding:0;display:grid;gap:8px;max-height:220px;overflow:auto}
li.strokeItem{display:flex;align-items:center;gap:8px;background:#0f121a;border:1px solid #2a3042;border-radius:10px;padding:8px}
.chip{display:inline-flex;align-items:center;gap:8px;background:#0f121a;border:1px dashed #2a3042;border-radius:10px;padding:6px 10px}
</style>
</head>
<body>
<header>
<h1>OneLiner Painter (MultiStroke) → SVG & Motion JSON • a→Spirale→y • Start/Stop/Dynamik</h1>
</header>
<div class="wrap">
<aside class="panel" aria-labelledby="controlsTitle">
<h2 id="controlsTitle" style="margin:0;font-size:1rem">Werkzeug</h2>
<div class="row">
<label class="chip"><input type="checkbox" id="sparkMode"> <span>Funken setzen (Shift)</span></label>
<label class="chip"><input type="checkbox" id="bitMode"> <span>Bits 1+1 setzen</span></label>
<label class="chip"><input type="checkbox" id="markers"> <span>Start/StopMarker</span></label>
</div>
<div class="row">
<label>Strichstärke
<input type="range" id="stroke" min="4" max="28" step="1" value="12">
</label>
<label>Glättung
<input type="range" id="smooth" min="0" max="10" step="1" value="2">
</label>
</div>
<div class="row">
<label>Farbe A <input type="color" id="colorA" value="#62d3a4"></label>
<label>Farbe B <input type="color" id="colorB" value="#ffd166"></label>
<label>Funken <input type="color" id="colorSpark" value="#e7eaf0"></label>
</div>
<div class="row">
<label class="chip"><input type="radio" name="mode" value="A" checked> <span>StrokeFarbe A</span></label>
<label class="chip"><input type="radio" name="mode" value="B"> <span>StrokeFarbe B</span></label>
<label class="chip"><input type="radio" name="mode" value="GRAD"> <span>Verlauf A→B</span></label>
</div>
<div class="row">
<label class="chip"><input type="checkbox" id="dynWidth"> <span>Breite ~ Geschwindigkeit (JSONonly)</span></label>
<label class="chip"><input type="checkbox" id="captureJSON" checked> <span>Motion JSON mitschreiben</span></label>
</div>
<div>
<strong>Strokes</strong>
<ul id="strokeList" aria-label="StrichListe"></ul>
<p class="muted">Neuer Stroke beginnt mit <span class="kbd">Maus/TouchDown</span>. Ende bei Loslassen. Mehrere Klicks/Wege werden einzeln erfasst.</p>
</div>
<div class="row">
<button class="btn" id="undo">Letzten Stroke löschen</button>
<button class="btn warn" id="clear">Alles löschen</button>
</div>
<div class="row">
<button class="btn" id="copySVG">SVG kopieren</button>
<button class="btn" id="downloadSVG">SVG speichern</button>
<button class="btn accent" id="copyJSON">MotionJSON kopieren</button>
<button class="btn" id="downloadJSON">JSON speichern</button>
</div>
<details>
<summary class="muted">A11y/Meta</summary>
<label>Titel <input id="title" type="text" value="Omi Omega OneLiner"></label>
<label>Beschreibung
<textarea id="desc" rows="3">Cursives kleines a wird zur Spirale; zwei Einsen im a; y hält zusammen; Funken ringsum.</textarea>
</label>
<label>Tag (z.B. Datum/Hashtag) <input id="tag" type="text" value="22.08.25 #CRUMB"></label>
</details>
<p class="muted">Tipps: Ziehen = zeichnen. <span class="kbd">Shift</span> = Funken. <span class="kbd">Z</span> = Undo. <span class="kbd">S</span> speichern.</p>
</aside>
<main class="canvasWrap" aria-label="Zeichenfläche">
<svg id="stage" viewBox="0 0 1200 800" role="img" aria-labelledby="svgTitle svgDesc">
<title id="svgTitle">Omi Omega OneLiner</title>
<desc id="svgDesc">Mehrere Pfade mit Start/Stop und Zeitdynamik: a→Spirale→y; Bits 1+1; Funken.</desc>
<defs>
<linearGradient id="gradA2B" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#62d3a4" />
<stop offset="50%" stop-color="#62d3a4" />
<stop offset="51%" stop-color="#ffd166" />
<stop offset="100%" stop-color="#ffd166" />
</linearGradient>
<style>
.sline{fill:none;stroke-linecap:round;stroke-linejoin:round}
.markerStart{fill:#35c759;stroke:none}
.markerStop{fill:#ff3b30;stroke:none}
</style>
</defs>
<g id="art">
<g id="strokes"></g>
<g id="bits"></g>
<g id="sparks" stroke="#e7eaf0" stroke-width="8" stroke-linecap="round"></g>
<g id="markersG"></g>
<text id="tagText" x="860" y="760" font-family="ui-monospace,monospace" font-size="22" fill="#cfd5e3"></text>
</g>
</svg>
</main>
</div>
<footer>
<span class="muted">Mehrere Klicks & Pfade sind erlaubt. Start/Stop wird als Ereignis erfasst; Geschwindigkeiten landen im MotionJSON.</span>
</footer>
<script>
(function(){
const stage = document.getElementById('stage');
const strokesG= document.getElementById('strokes');
const bitsG = document.getElementById('bits');
const sparksG = document.getElementById('sparks');
const markersG= document.getElementById('markersG');
const grad = document.getElementById('gradA2B');
const tagText = document.getElementById('tagText');
const strokeList = document.getElementById('strokeList');
// Controls
const sparkMode = document.getElementById('sparkMode');
const bitMode = document.getElementById('bitMode');
const markersCb = document.getElementById('markers');
const strokeInp = document.getElementById('stroke');
const smoothInp = document.getElementById('smooth');
const colorAInp = document.getElementById('colorA');
const colorBInp = document.getElementById('colorB');
const colorSInp = document.getElementById('colorSpark');
const dynWidth = document.getElementById('dynWidth');
const captureJSON = document.getElementById('captureJSON');
const titleInp = document.getElementById('title');
const descInp = document.getElementById('desc');
const tagInp = document.getElementById('tag');
const undoBtn = document.getElementById('undo');
const clearBtn = document.getElementById('clear');
const copySVGBtn = document.getElementById('copySVG');
const dlSVGBtn = document.getElementById('downloadSVG');
const copyJSONBtn = document.getElementById('copyJSON');
const dlJSONBtn = document.getElementById('downloadJSON');
let mode = 'A';
document.querySelectorAll('input[name="mode"]').forEach(r=>{
r.addEventListener('change', ()=>{ mode = document.querySelector('input[name="mode"]:checked').value; });
});
// State
let drawing = false;
let curStroke = null; // {id, mode, color, width, points:[{x,y,t}], pathEl}
const strokes = []; // array of strokes
let t0 = null; // session start time
function now(){ return performance.now(); }
function svgPoint(evt){
const pt = stage.createSVGPoint();
pt.x = evt.clientX; pt.y = evt.clientY;
const ctm = stage.getScreenCTM().inverse();
const p = pt.matrixTransform(ctm);
return {x: Math.round(p.x), y: Math.round(p.y)};
}
function updateMeta(){
stage.querySelector('title').textContent = titleInp.value.trim() || 'OneLiner';
stage.querySelector('desc').textContent = descInp.value.trim() || '';
tagText.textContent = tagInp.value.trim();
}
function listRefresh(){
strokeList.innerHTML = '';
strokes.forEach((s,i)=>{
const li = document.createElement('li'); li.className='strokeItem';
const sw = document.createElement('input'); sw.type='range'; sw.min=4; sw.max=28; sw.step=1; sw.value=s.width; sw.title='Breite';
sw.addEventListener('input',()=>{ s.width=+sw.value; s.pathEl.setAttribute('stroke-width', s.width); });
const sel = document.createElement('select');
['A','B','GRAD'].forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; if(s.mode===v) o.selected=true; sel.appendChild(o); });
sel.addEventListener('change',()=>{ s.mode=sel.value; applyStrokeStyle(s); });
const del = document.createElement('button'); del.className='btn'; del.textContent='✕';
del.addEventListener('click',()=>{ removeStroke(i); });
const meta = document.createElement('span'); meta.className='muted';
meta.textContent = `#${s.id}${Math.round(s.duration)}ms • ~${Math.round(s.length)}px`;
li.append('Stroke', sel, sw, del, meta);
strokeList.appendChild(li);
});
}
function removeStroke(idx){
const s = strokes[idx];
if(!s) return;
s.pathEl.remove(); if(s.startMarker) s.startMarker.remove(); if(s.stopMarker) s.stopMarker.remove();
strokes.splice(idx,1);
listRefresh();
}
function createPathEl(){
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('class','sline');
p.setAttribute('stroke-width', strokeInp.value);
strokesG.appendChild(p);
return p;
}
function applyStrokeStyle(s){
if(s.mode==='A'){ s.pathEl.setAttribute('stroke', colorAInp.value); }
else if(s.mode==='B'){ s.pathEl.setAttribute('stroke', colorBInp.value); }
else { s.pathEl.setAttribute('stroke','url(#gradA2B)'); }
}
function toPathD(points){
if(points.length===0) return '';
const sm = +smoothInp.value;
if(points.length<3 || sm===0){
const p0 = points[0];
let d = `M ${p0.x} ${p0.y}`;
for(let i=1;i<points.length;i++){ const p=points[i]; d += ` L ${p.x} ${p.y}`; }
return d;
}
// simple smoothing: use quadratic Beziers between midpoints
let d = `M ${points[0].x} ${points[0].y}`;
for(let i=1;i<points.length-1;i++){
const p0 = points[i];
const p1 = points[i+1];
const mx = (p0.x + p1.x)/2; const my = (p0.y + p1.y)/2;
d += ` Q ${p0.x} ${p0.y} ${mx} ${my}`;
}
const last = points[points.length-1]; d += ` L ${last.x} ${last.y}`;
return d;
}
function startStroke(e){
if(bitMode.checked){ placeBit(e); return; }
if(sparkMode.checked || e.shiftKey){ placeSpark(e); return; }
drawing = true;
if(t0===null) t0 = now();
const t = now() - t0;
const pt = svgPoint(e);
const pathEl = createPathEl();
curStroke = { id: Date.now()%1e7, mode, colorA: colorAInp.value, colorB: colorBInp.value, width:+strokeInp.value, points:[{...pt,t}], pathEl, start:t, duration:0, length:0 };
applyStrokeStyle(curStroke);
updatePath(curStroke);
if(markersCb.checked){ curStroke.startMarker = mark(pt.x, pt.y, 'start'); }
window.addEventListener('pointerup', endStroke, {once:true});
}
function moveStroke(e){
if(!drawing || !curStroke) return;
const p = svgPoint(e);
const t = now() - t0;
const last = curStroke.points[curStroke.points.length-1];
if(!last || Math.hypot(p.x-last.x, p.y-last.y) > 2){
curStroke.points.push({...p,t});
updatePath(curStroke);
}
}
function endStroke(){
if(!curStroke) return;
drawing = false;
const pts = curStroke.points;
curStroke.duration = pts.length? (pts[pts.length-1].t - pts[0].t) : 0;
curStroke.length = polyLen(pts);
if(markersCb.checked){ const last=pts[pts.length-1]; curStroke.stopMarker = mark(last.x,last.y,'stop'); }
strokes.push(curStroke); curStroke = null; listRefresh();
}
function mark(x,y,type){
const r = 7; const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class', type==='start'?'markerStart':'markerStop');
markersG.appendChild(c); return c;
}
function updatePath(s){ s.pathEl.setAttribute('d', toPathD(s.points)); s.pathEl.setAttribute('stroke-width', s.width); }
function polyLen(pts){ let L=0; for(let i=1;i<pts.length;i++){ const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; L += Math.hypot(dx,dy);} return L; }
function placeSpark(e){
const p = svgPoint(e);
const len = Math.max(14, parseInt(strokeInp.value,10)*1.5);
const dx = 10, dy = -10;
const l = document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1', p.x); l.setAttribute('y1', p.y);
l.setAttribute('x2', p.x+dx); l.setAttribute('y2', p.y+dy);
l.setAttribute('stroke', colorSInp.value);
l.setAttribute('stroke-width', Math.max(2, Math.round(parseInt(strokeInp.value,10)*0.66)));
l.setAttribute('stroke-linecap','round');
sparksG.appendChild(l);
}
function placeBit(e){
const p = svgPoint(e);
const h = Math.max(18, parseInt(strokeInp.value,10)*1.2);
const w = Math.max(6, parseInt(strokeInp.value,10)*0.8);
const line1 = document.createElementNS('http://www.w3.org/2000/svg','line');
line1.setAttribute('x1', p.x); line1.setAttribute('y1', p.y-h/2);
line1.setAttribute('x2', p.x); line1.setAttribute('y2', p.y+h/2);
line1.setAttribute('stroke', colorBInp.value);
line1.setAttribute('stroke-width', w);
line1.setAttribute('stroke-linecap', 'round');
bitsG.appendChild(line1);
}
function exportSVGString(){
updateMeta();
const clone = stage.cloneNode(true);
// inline live colors
clone.querySelector('#sparks')?.setAttribute('stroke', colorSInp.value);
// serialize
const s = new XMLSerializer().serializeToString(clone);
return `<?xml version="1.0" encoding="UTF-8"?>
${s}`;
}
function motionJSON(){
const meta = { title:titleInp.value, desc:descInp.value, tag:tagInp.value, t0: t0??0 };
const items = strokes.map(s=>{
// speeds
let maxV=0, sumV=0, nV=0; const pts=s.points;
for(let i=1;i<pts.length;i++){
const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; const dt=(pts[i].t-pts[i-1].t)/1000; if(dt<=0) continue;
const v = Math.hypot(dx,dy)/dt; maxV=Math.max(maxV,v); sumV+=v; nV++;
}
const avgV = nV? sumV/nV : 0;
return {
id: s.id, mode: s.mode, colorA: s.colorA, colorB: s.colorB, width: s.width,
startMs: s.start, durationMs: s.duration, lengthPx: s.length,
avgSpeedPxPerS: +avgV.toFixed(2), maxSpeedPxPerS: +maxV.toFixed(2),
points: s.points.map(p=>({x:p.x,y:p.y,tMs: Math.round(p.t)}))
};
});
return JSON.stringify({meta, strokes:items}, null, 2);
}
// Buttons
document.getElementById('undo').addEventListener('click', ()=>{ if(strokes.length){ removeStroke(strokes.length-1); }});
document.getElementById('clear').addEventListener('click', ()=>{
strokes.length=0; strokesG.innerHTML=''; bitsG.innerHTML=''; sparksG.innerHTML=''; markersG.innerHTML=''; listRefresh(); t0=null; });
copySVGBtn.addEventListener('click', async ()=>{ const svg=exportSVGString(); await navigator.clipboard.writeText(svg); alert('SVG kopiert'); });
dlSVGBtn.addEventListener('click', ()=>{ const svg=exportSVGString(); const blob=new Blob([svg],{type:'image/svg+xml'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega-oneliner_multi.svg'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
copyJSONBtn.addEventListener('click', async ()=>{ const js=motionJSON(); await navigator.clipboard.writeText(js); alert('MotionJSON kopiert'); });
dlJSONBtn.addEventListener('click', ()=>{ const js=motionJSON(); const blob=new Blob([js],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega_motion.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
// Stage events
stage.addEventListener('pointerdown', startStroke);
stage.addEventListener('pointermove', moveStroke);
window.addEventListener('pointerup', endStroke);
// Hotkeys
window.addEventListener('keydown', (e)=>{
if(e.key==='z' || e.key==='Z'){ if(strokes.length){ removeStroke(strokes.length-1); } }
if((e.key==='s' || e.key==='S') && (e.ctrlKey||e.metaKey)){ e.preventDefault(); document.getElementById('downloadSVG').click(); }
});
// Reactive
[titleInp,descInp,tagInp].forEach(inp=>inp.addEventListener('input', updateMeta));
[colorAInp,colorBInp,colorSInp].forEach(inp=>inp.addEventListener('input', ()=>{ tagText.setAttribute('fill','#cfd5e3'); }));
updateMeta();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,647 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>One-Liner Painter + Bezier-Editor (Fix)</title>
<style>
:root{ --bg:#0b0b10; --panel:#12121a; --muted:#9aa3b2; --accent:#62d3a4; --accent2:#ffd166; --danger:#ff5c5c; }
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;display:grid;grid-template-rows:auto 1fr auto;background:var(--bg);color:#e7eaf0;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
header,footer{padding:12px 16px;background:var(--panel);border-bottom:1px solid #1e2230}
footer{border-top:1px solid #1e2230;border-bottom:none;font-size:.9rem;color:var(--muted)}
h1{font-size:1.05rem;margin:0}
.wrap{display:grid;grid-template-columns:370px 1fr;gap:16px;padding:16px}
@media (max-width:1000px){.wrap{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid #1e2230;border-radius:12px;padding:14px;display:grid;gap:12px}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.col{display:grid;gap:10px}
label{font-size:.9rem;color:#cfd5e3}
input[type="text"],textarea,select{width:100%;padding:10px;border-radius:10px;border:1px solid #2a3042;background:#0f121a;color:#e7eaf0}
input[type="color"]{width:48px;height:36px;border:none;background:none}
.btn{appearance:none;border:none;border-radius:10px;padding:10px 12px;background:#1f2636;color:#e7eaf0;cursor:pointer}
.btn:hover{background:#28314a}
.btn.accent{background:var(--accent);color:#0b1114;font-weight:600}
.btn.warn{background:var(--danger);color:#0b0b10}
.chip{display:inline-flex;align-items:center;gap:8px;background:#0f121a;border:1px dashed #2a3042;border-radius:10px;padding:6px 10px}
.kbd{padding:1px 6px;border-radius:8px;background:#1e2333;color:#cfd5e3;font-family:ui-monospace,Menlo,Monaco,monospace}
ul#strokeList{list-style:none;margin:0;padding:0;display:grid;gap:8px;max-height:240px;overflow:auto}
li.strokeItem{display:flex;align-items:center;gap:8px;background:#0f121a;border:1px solid #2a3042;border-radius:10px;padding:8px}
.canvasWrap{position:relative;background:#0f121a;border:1px solid #1e2230;border-radius:12px;overflow:hidden}
svg{display:block;width:100%;height:100%;background:#0f121a}
.badge{font-size:.75rem;color:#0b1114;background:var(--accent);border-radius:999px;padding:2px 8px}
.muted{color:var(--muted)}
/* Bezier UI visuals */
.sline{fill:none;stroke-linecap:round;stroke-linejoin:round}
.markerStart{fill:#35c759;stroke:none}
.markerStop{fill:#ff3b30;stroke:none}
.anchor{fill:#31e07b;stroke:#0b0b10;stroke-width:2;cursor:grab; pointer-events:all}
.handle{fill:#ffe08a;stroke:#0b0b10;stroke-width:2;cursor:grab; pointer-events:all}
.handleLine{stroke:#ffe08a;stroke-width:2.5;stroke-dasharray:4 4; pointer-events:none}
.selected{filter:drop-shadow(0 0 4px #ffd166)}
#bezierUI{ pointer-events:none } /* UI-Schicht blockiert keine Klicks auf Pfad */
</style>
</head>
<body>
<header>
<h1>One-Liner Painter → SVG/PNG/JSON • Replay • <span class="badge">Bezier-Editor (Fix)</span></h1>
</header>
<div class="wrap">
<aside class="panel" aria-labelledby="controlsTitle">
<h2 id="controlsTitle" style="margin:0;font-size:1rem">Werkzeug</h2>
<div class="row">
<label class="chip"><input type="checkbox" id="sparkMode"> <span>Funken (Shift)</span></label>
<label class="chip"><input type="checkbox" id="bitMode"> <span>Bits „1“ setzen</span></label>
<label class="chip"><input type="checkbox" id="markers"> <span>Start/Stop-Marker</span></label>
<label class="chip"><input type="checkbox" id="frameOn" checked> <span>Crumblines-Frame</span></label>
</div>
<div class="row">
<label>Strichstärke <input type="range" id="stroke" min="4" max="28" step="1" value="12"></label>
<label>Sampler
<select id="sampler">
<option value="raw">Raw Polyline</option>
<option value="quad">Quadratic</option>
<option value="cubic" selected>Catmull-Rom → Cubic</option>
</select>
</label>
</div>
<div class="row">
<label>Farbe A <input type="color" id="colorA" value="#62d3a4"></label>
<label>Farbe B <input type="color" id="colorB" value="#ffd166"></label>
<label>Funken <input type="color" id="colorSpark" value="#e7eaf0"></label>
</div>
<div class="row">
<label class="chip"><input type="radio" name="mode" value="A" checked> <span>Stroke A</span></label>
<label class="chip"><input type="radio" name="mode" value="B"> <span>Stroke B</span></label>
<label class="chip"><input type="radio" name="mode" value="GRAD"> <span>Verlauf A→B</span></label>
</div>
<div class="col" style="margin-top:6px">
<strong>Bezier-Editor</strong>
<div class="row">
<label class="chip"><input type="checkbox" id="editBezier"> <span>Bearbeiten</span></label>
<label class="chip"><input type="checkbox" id="linkHandles" checked> <span>Spiegel-Griffe</span></label>
<button class="btn" id="toBezier">Auswahl → Bezier</button>
<button class="btn" id="splitHandles">Anker: Split</button>
<button class="btn warn" id="delAnchor" disabled>Anker löschen</button>
</div>
<p class="muted">Klicke einen Stroke, dann <em>Bearbeiten</em>. Grüne Punkte = Anker, gelbe = Griffe. Ziehen. Doppelklick = Anker einfügen. <span class="kbd">Entf</span> löscht. <span class="kbd">L</span> spiegelt Griffe.</p>
</div>
<div>
<strong>Strokes</strong>
<ul id="strokeList" aria-label="Strich-Liste"></ul>
</div>
<div class="row">
<button class="btn" id="undo">Letzten Stroke löschen</button>
<button class="btn warn" id="clear">Alles löschen</button>
</div>
<div class="row">
<button class="btn" id="replay">▶︎ Replay</button>
<button class="btn" id="copySVG">SVG kopieren</button>
<button class="btn" id="downloadSVG">SVG speichern</button>
<button class="btn" id="downloadPNG">PNG speichern</button>
<button class="btn accent" id="copyJSON">Motion-JSON kopieren</button>
<button class="btn" id="downloadJSON">JSON speichern</button>
</div>
</aside>
<main class="canvasWrap" aria-label="Zeichenfläche">
<svg id="stage" viewBox="0 0 1200 800" role="img" aria-labelledby="svgTitle svgDesc">
<title id="svgTitle">Omi Omega One-Liner</title>
<desc id="svgDesc">a→Spirale→y; Bits 1+1; Funken.</desc>
<defs>
<linearGradient id="gradA2B" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#62d3a4" />
<stop offset="50%" stop-color="#62d3a4" />
<stop offset="51%" stop-color="#ffd166" />
<stop offset="100%" stop-color="#ffd166" />
</linearGradient>
<style>
.selected{filter:drop-shadow(0 0 4px #ffd166)}
</style>
</defs>
<rect id="frameRect" x="16.5" y="16.5" width="1167" height="767" fill="none" stroke="rgba(255,255,255,.2)" stroke-width="1.5" stroke-dasharray="6 10"/>
<g id="art">
<g id="strokes"></g>
<g id="bits"></g>
<g id="sparks" stroke="#e7eaf0" stroke-width="8" stroke-linecap="round"></g>
<g id="markersG"></g>
<g id="bezierUI"></g>
</g>
</svg>
</main>
</div>
<footer>
<span>Fixes: keine (0,0)-Griffe mehr; Drag startet sofort; UI blockiert keine Klicks.</span>
</footer>
<script>
(function(){
const stage = document.getElementById('stage');
const strokesG= document.getElementById('strokes');
const bitsG = document.getElementById('bits');
const sparksG = document.getElementById('sparks');
const markersG= document.getElementById('markersG');
const bezierUI= document.getElementById('bezierUI');
const frameRect = document.getElementById('frameRect');
const strokeList = document.getElementById('strokeList');
// Controls
const sparkMode = document.getElementById('sparkMode');
const bitMode = document.getElementById('bitMode');
const markersCb = document.getElementById('markers');
const frameOnCb = document.getElementById('frameOn');
const strokeInp = document.getElementById('stroke');
const samplerSel= document.getElementById('sampler');
const colorAInp = document.getElementById('colorA');
const colorBInp = document.getElementById('colorB');
const colorSInp = document.getElementById('colorSpark');
const editBezierCb = document.getElementById('editBezier');
const linkHandlesCb= document.getElementById('linkHandles');
const toBezierBtn = document.getElementById('toBezier');
const splitBtn = document.getElementById('splitHandles');
const delAnchorBtn = document.getElementById('delAnchor');
const undoBtn = document.getElementById('undo');
const clearBtn = document.getElementById('clear');
const copySVGBtn = document.getElementById('copySVG');
const dlSVGBtn = document.getElementById('downloadSVG');
const dlPNGBtn = document.getElementById('downloadPNG');
const copyJSONBtn = document.getElementById('copyJSON');
const dlJSONBtn = document.getElementById('downloadJSON');
const replayBtn = document.getElementById('replay');
let mode = 'A';
document.querySelectorAll('input[name="mode"]').forEach(r=>{
r.addEventListener('change', ()=>{ mode = document.querySelector('input[name="mode"]:checked').value; });
});
// State
let drawing = false;
let curStroke = null;
const strokes = [];
let t0 = null;
let playing = false;
let selectedStroke = null;
let selectedAnchor = {stroke:null, idx:-1};
let dragging = null; // {type:'anchor'|'h1'|'h2', stroke, idx, ox, oy, pid}
function now(){ return performance.now(); }
function svgPoint(evt){
const pt = stage.createSVGPoint();
pt.x = evt.clientX; pt.y = evt.clientY;
const ctm = stage.getScreenCTM().inverse();
const p = pt.matrixTransform(ctm);
return {x: Math.round(p.x), y: Math.round(p.y)};
}
function listRefresh(){
strokeList.innerHTML = '';
strokes.forEach((s,i)=>{
const li = document.createElement('li'); li.className='strokeItem';
const sw = document.createElement('input'); sw.type='range'; sw.min=4; sw.max=28; sw.step=1; sw.value=s.width; sw.title='Breite';
sw.addEventListener('input',()=>{ s.width=+sw.value; s.pathEl.setAttribute('stroke-width', s.width); });
const sel = document.createElement('select');
['A','B','GRAD'].forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; if(s.mode===v) o.selected=true; sel.appendChild(o); });
sel.addEventListener('change',()=>{ s.mode=sel.value; applyStrokeStyle(s); });
const edit = document.createElement('button'); edit.className='btn'; edit.textContent='Bearbeiten';
edit.addEventListener('click',()=>{ selectStroke(s); editBezierCb.checked = true; drawBezierUI(); });
const del = document.createElement('button'); del.className='btn'; del.textContent='✕';
del.addEventListener('click',()=>{ removeStroke(i); });
const meta = document.createElement('span'); meta.className='muted';
meta.textContent = `#${s.id}${Math.round(s.duration)}ms • ~${Math.round(s.length)}px${s.isBezier?' • Bezier':''}`;
li.append('Stroke', sel, sw, edit, del, meta);
strokeList.appendChild(li);
});
}
function removeStroke(idx){
const s = strokes[idx];
if(!s) return;
s.pathEl.remove(); if(s.startMarker) s.startMarker.remove(); if(s.stopMarker) s.stopMarker.remove();
if(selectedStroke===s){ selectedStroke=null; clearBezierUI(); }
strokes.splice(idx,1);
listRefresh();
}
function createPathEl(){
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('class','sline');
p.setAttribute('stroke-width', strokeInp.value);
p.addEventListener('pointerdown', (e)=>{
if(editBezierCb.checked){ e.stopPropagation(); const s = strokes.find(st=>st.pathEl===p); if(s){ selectStroke(s); drawBezierUI(); } }
});
strokesG.appendChild(p);
return p;
}
function applyStrokeStyle(s){
if(s.mode==='A'){ s.pathEl.setAttribute('stroke', colorAInp.value); }
else if(s.mode==='B'){ s.pathEl.setAttribute('stroke', colorBInp.value); }
else { s.pathEl.setAttribute('stroke','url(#gradA2B)'); }
}
// Samplers → SVG path "d"
function pathRaw(points){
if(points.length===0) return '';
const p0 = points[0];
let d = `M ${p0.x} ${p0.y}`;
for(let i=1;i<points.length;i++){ const p=points[i]; d += ` L ${p.x} ${p.y}`; }
return d;
}
function pathQuadratic(points){
if(points.length<2) return '';
let d = `M ${points[0].x} ${points[0].y}`;
for(let i=1;i<points.length-1;i++){
const p0 = points[i]; const p1 = points[i+1];
const mx = (p0.x + p1.x)/2; const my = (p0.y + p1.y)/2;
d += ` Q ${p0.x} ${p0.y} ${mx} ${my}`;
}
const last = points[points.length-1]; d += ` L ${last.x} ${last.y}`;
return d;
}
function pathCubic(points){
if(points.length<2) return '';
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
const p=(i)=>points[ clamp(i,0,points.length-1) ];
let d=`M ${points[0].x} ${points[0].y}`;
for(let i=0;i<points.length-1;i++){
const p0=p(i-1), p1=p(i), p2=p(i+1), p3=p(i+2);
const c1x = p1.x + (p2.x - p0.x)/6, c1y = p1.y + (p2.y - p0.y)/6;
const c2x = p2.x - (p3.x - p1.x)/6, c2y = p2.y - (p3.y - p1.y)/6;
d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2.x} ${p2.y}`;
}
return d;
}
function toPathD(s){
if(s.isBezier) return pathFromAnchors(s.anchors);
const pts = s.points;
const smp = s.sampler || samplerSel.value;
if(smp==='raw') return pathRaw(pts);
if(smp==='cubic') return pathCubic(pts);
return pathQuadratic(pts);
}
function startStroke(e){
if(editBezierCb.checked){ return; } // während Edit nicht zeichnen
if(bitMode.checked){ placeBit(e); return; }
if(sparkMode.checked || e.shiftKey){ placeSpark(e); return; }
drawing = true;
if(t0===null) t0 = now();
const t = now() - t0;
const pt = svgPoint(e);
const pathEl = createPathEl();
curStroke = { id: Date.now()%1e7, mode, colorA: colorAInp.value, colorB: colorBInp.value, width:+strokeInp.value, points:[{...pt,t}], pathEl, start:t, duration:0, length:0, sampler:samplerSel.value, isBezier:false };
applyStrokeStyle(curStroke);
updatePath(curStroke);
if(markersCb.checked){ curStroke.startMarker = mark(pt.x, pt.y, 'start'); }
window.addEventListener('pointerup', endStroke, {once:true});
}
function moveStroke(e){
if(!drawing || !curStroke) return;
const p = svgPoint(e);
const t = now() - t0;
const last = curStroke.points[curStroke.points.length-1];
if(!last || Math.hypot(p.x-last.x, p.y-last.y) > 2){
curStroke.points.push({...p,t});
updatePath(curStroke);
}
}
function endStroke(){
if(!curStroke) return;
drawing = false;
const pts = curStroke.points;
curStroke.duration = pts.length? (pts[pts.length-1].t - pts[0].t) : 0;
curStroke.length = polyLen(pts);
if(markersCb.checked){ const last=pts[pts.length-1]; curStroke.stopMarker = mark(last.x,last.y,'stop'); }
strokes.push(curStroke); curStroke = null; listRefresh();
}
function mark(x,y,type){
const r = 7; const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class', type==='start'?'markerStart':'markerStop');
markersG.appendChild(c); return c;
}
function polyLen(pts){ let L=0; for(let i=1;i<pts.length;i++){ const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; L += Math.hypot(dx,dy);} return L; }
function updatePath(s){
s.pathEl.setAttribute('d', toPathD(s));
s.pathEl.setAttribute('stroke-width', s.width);
}
function placeSpark(e){
const p = svgPoint(e);
const dx = 10, dy = -10;
const l = document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1', p.x); l.setAttribute('y1', p.y);
l.setAttribute('x2', p.x+dx); l.setAttribute('y2', p.y+dy);
l.setAttribute('stroke', colorSInp.value);
l.setAttribute('stroke-width', Math.max(2, Math.round(parseInt(strokeInp.value,10)*0.66)));
l.setAttribute('stroke-linecap','round');
sparksG.appendChild(l);
}
function placeBit(e){
const p = svgPoint(e);
const h = Math.max(18, parseInt(strokeInp.value,10)*1.2);
const w = Math.max(6, parseInt(strokeInp.value,10)*0.8);
const line1 = document.createElementNS('http://www.w3.org/2000/svg','line');
line1.setAttribute('x1', p.x); line1.setAttribute('y1', p.y-h/2);
line1.setAttribute('x2', p.x); line1.setAttribute('y2', p.y+h/2);
line1.setAttribute('stroke', colorBInp.value);
line1.setAttribute('stroke-width', w);
line1.setAttribute('stroke-linecap', 'round');
bitsG.appendChild(line1);
}
// --- Bezier conversion & editing ---
// FIX: build anchors with valid handles only; no (0,0) placeholders
function catmullToAnchors(points){
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
const P=(i)=>points[ clamp(i,0,points.length-1) ];
const A=[];
for(let i=0;i<points.length-1;i++){
const p0=P(i-1), p1=P(i), p2=P(i+1), p3=P(i+2);
const c1 = {x: p1.x + (p2.x - p0.x)/6, y: p1.y + (p2.y - p0.y)/6};
const c2 = {x: p2.x - (p3.x - p1.x)/6, y: p2.y - (p3.y - p1.y)/6};
if(i===0){
A.push({x:p1.x,y:p1.y,h1:{x:p1.x,y:p1.y},h2:{x:c1.x,y:c1.y},split:false});
}else{
// update previous anchor's outgoing handle
A[A.length-1].h2 = {x:c1.x,y:c1.y};
}
// next anchor for p2
const next = {x:p2.x,y:p2.y,h1:{x:c2.x,y:c2.y},h2:{x:p2.x,y:p2.y},split:false};
A.push(next);
}
return A;
}
function pathFromAnchors(A){
if(!A || A.length<2) return '';
let d = `M ${A[0].x} ${A[0].y}`;
for(let i=0;i<A.length-1;i++){
const a=A[i], b=A[i+1];
d += ` C ${a.h2.x} ${a.h2.y}, ${b.h1.x} ${b.h1.y}, ${b.x} ${b.y}`;
}
return d;
}
function selectStroke(s){
selectedStroke = s;
strokes.forEach(st=> st.pathEl.classList.toggle('selected', st===s));
}
function clearBezierUI(){ bezierUI.innerHTML=''; selectedAnchor={stroke:null, idx:-1}; delAnchorBtn.disabled = true; }
function drawBezierUI(){
clearBezierUI();
const s = selectedStroke;
if(!editBezierCb.checked || !s || !s.isBezier) return;
s.anchors.forEach((a,idx)=>{
// handle lines
bezierUI.append(line(a.x, a.y, a.h1.x, a.h1.y, 'handleLine'));
bezierUI.append(line(a.x, a.y, a.h2.x, a.h2.y, 'handleLine'));
// handles
const h1 = circle(a.h1.x, a.h1.y, 5, 'handle', (e)=>startDrag('h1', s, idx, e));
const h2 = circle(a.h2.x, a.h2.y, 5, 'handle', (e)=>startDrag('h2', s, idx, e));
bezierUI.append(h1, h2);
// anchor
const an = circle(a.x, a.y, 6.5, 'anchor', (e)=>{ startDrag('anchor', s, idx, e) });
an.addEventListener('dblclick', (e)=>{ e.stopPropagation(); insertAnchorAt(s, idx); });
an.addEventListener('pointerdown', ()=>{ selectedAnchor={stroke:s,idx}; delAnchorBtn.disabled=false; });
bezierUI.append(an);
});
}
function line(x1,y1,x2,y2,cls){
const l = document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1',x1); l.setAttribute('y1',y1);
l.setAttribute('x2',x2); l.setAttribute('y2',y2);
l.setAttribute('class',cls);
return l;
}
function circle(x,y,r,cls,onDown){
const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class',cls);
c.style.touchAction = 'none';
c.addEventListener('pointerdown', (e)=>{ e.stopPropagation(); onDown(e); });
return c;
}
// FIX: start drag immediately on current event + proper pointer capture
function startDrag(type, s, idx, ev){
ev.preventDefault();
const a = s.anchors[idx];
const p0 = svgPoint(ev);
dragging = {type, stroke:s, idx, ox:p0.x, oy:p0.y, pid:ev.pointerId};
try { stage.setPointerCapture(ev.pointerId); } catch(e){}
const move = (e)=>{
if(!dragging) return;
const pos = svgPoint(e);
const dx = pos.x - dragging.ox, dy = pos.y - dragging.oy;
dragging.ox = pos.x; dragging.oy = pos.y;
if(type==='anchor'){
a.x+=dx; a.y+=dy; a.h1.x+=dx; a.h1.y+=dy; a.h2.x+=dx; a.h2.y+=dy;
}else if(type==='h1'){
a.h1.x+=dx; a.h1.y+=dy;
if(linkHandlesCb.checked && !a.split){
a.h2.x = a.x - (a.h1.x - a.x);
a.h2.y = a.y - (a.h1.y - a.y);
}
}else if(type==='h2'){
a.h2.x+=dx; a.h2.y+=dy;
if(linkHandlesCb.checked && !a.split){
a.h1.x = a.x - (a.h2.x - a.x);
a.h1.y = a.y - (a.h2.y - a.y);
}
}
s.pathEl.setAttribute('d', pathFromAnchors(s.anchors));
drawBezierUI();
};
const up = (e)=>{
try { stage.releasePointerCapture(ev.pointerId); } catch(_) {}
window.removeEventListener('pointermove', move);
dragging=null;
};
window.addEventListener('pointermove', move, {passive:true});
window.addEventListener('pointerup', up, {once:true});
}
function insertAnchorAt(s, idx){
const A = s.anchors;
if(idx>=A.length-1) return;
const a = A[idx], b = A[idx+1];
const mid = {x:(a.x+b.x)/2, y:(a.y+b.y)/2};
const dir = {x:(b.x-a.x), y:(b.y-a.y)};
const scale = 0.33;
const hLen = {x:dir.x*scale, y:dir.y*scale};
const newA = {
x: mid.x, y: mid.y,
h1: {x: mid.x - hLen.x*0.5, y: mid.y - hLen.y*0.5},
h2: {x: mid.x + hLen.x*0.5, y: mid.y + hLen.y*0.5},
split:false
};
A.splice(idx+1,0,newA);
s.pathEl.setAttribute('d', pathFromAnchors(A));
drawBezierUI();
}
function deleteSelectedAnchor(){
const {stroke:s, idx} = selectedAnchor;
if(!s || idx<0) return;
if(s.anchors.length<=2) return;
s.anchors.splice(idx,1);
selectedAnchor={stroke:null,idx:-1};
delAnchorBtn.disabled=true;
s.pathEl.setAttribute('d', pathFromAnchors(s.anchors));
drawBezierUI();
}
function convertToBezier(s){
if(s.isBezier) return;
if(s.points.length<2) return;
s.anchors = catmullToAnchors(s.points);
s.isBezier = true;
s.pathEl.setAttribute('d', pathFromAnchors(s.anchors));
listRefresh();
}
toBezierBtn.addEventListener('click', ()=>{ if(selectedStroke) { convertToBezier(selectedStroke); drawBezierUI(); } });
splitBtn.addEventListener('click', ()=>{ if(selectedStroke && selectedAnchor.idx>=0){ const a=selectedStroke.anchors[selectedAnchor.idx]; a.split=!a.split; drawBezierUI(); }});
delAnchorBtn.addEventListener('click', deleteSelectedAnchor);
window.addEventListener('keydown', (e)=>{
if(e.key==='Delete' || e.key==='Backspace'){ if(editBezierCb.checked) { deleteSelectedAnchor(); } }
if(e.key==='l' || e.key==='L'){ linkHandlesCb.checked = !linkHandlesCb.checked; }
});
// Double-click on path to insert anchor near click
strokesG.addEventListener('dblclick', (e)=>{
if(!editBezierCb.checked || !selectedStroke || !selectedStroke.isBezier) return;
const p = svgPoint(e);
let bestI=0, bestD=1e9, A=selectedStroke.anchors;
for(let i=0;i<A.length-1;i++){
const a=A[i], b=A[i+1];
const d = Math.hypot(p.x-(a.x+b.x)/2, p.y-(a.y+b.y)/2);
if(d<bestD){ bestD=d; bestI=i; }
}
insertAnchorAt(selectedStroke, bestI);
});
// Export / Import / Replay (minimal for this fix)
function exportSVGString(){
frameRect.style.display = frameOnCb.checked ? 'block' : 'none';
const clone = stage.cloneNode(true);
// remove Bezier UI
const ui = clone.querySelector('#bezierUI'); if(ui) ui.remove();
const s = new XMLSerializer().serializeToString(clone);
return `<?xml version="1.0" encoding="UTF-8"?>\n${s}`;
}
function motionJSON(){
const items = strokes.map(s=>{
const o = {
id: s.id, mode: s.mode, colorA: s.colorA, colorB: s.colorB, width: s.width, sampler: s.sampler,
startMs: s.start||0, durationMs: s.duration||0, lengthPx: s.length||0,
};
if(s.isBezier){
o.isBezier = true;
o.anchors = s.anchors.map(a=>({x:a.x,y:a.y,h1:{x:a.h1.x,y:a.h1.y},h2:{x:a.h2.x,y:a.h2.y},split:!!a.split}));
}else{
o.points = (s.points||[]).map(p=>({x:p.x,y:p.y,tMs: Math.round(p.t)}));
}
return o;
});
return JSON.stringify({strokes:items}, null, 2);
}
document.getElementById('undo').addEventListener('click', ()=>{ if(strokes.length){ removeStroke(strokes.length-1); }});
document.getElementById('clear').addEventListener('click', ()=>{
strokes.length=0; strokesG.innerHTML=''; bitsG.innerHTML=''; sparksG.innerHTML=''; markersG.innerHTML=''; clearBezierUI(); listRefresh(); t0=null; });
copySVGBtn.addEventListener('click', async ()=>{ const svg=exportSVGString(); await navigator.clipboard.writeText(svg); alert('SVG kopiert'); });
dlSVGBtn.addEventListener('click', ()=>{ const svg=exportSVGString(); const blob=new Blob([svg],{type:'image/svg+xml'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega-oneliner.svg'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
dlPNGBtn.addEventListener('click', ()=>{ const svg=exportSVGString(); svgToPng(svg, 'omi-omega-oneliner.png'); });
copyJSONBtn.addEventListener('click', async ()=>{ const js=motionJSON(); await navigator.clipboard.writeText(js); alert('Motion-JSON kopiert'); });
dlJSONBtn.addEventListener('click', ()=>{ const js=motionJSON(); const blob=new Blob([js],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega_motion.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
replayBtn.addEventListener('click', startReplay);
// Stage events
stage.addEventListener('pointerdown', startStroke);
stage.addEventListener('pointermove', moveStroke);
window.addEventListener('pointerup', ()=>{ drawing=false; });
// init
frameRect.style.display = frameOnCb.checked ? 'block':'none';
// Replay
function startReplay(){
if(!strokes.length) return;
playing = true;
markersG.style.opacity = 0.25; bezierUI.style.opacity = 0.15;
strokes.forEach(s=> s.pathEl.setAttribute('d',''));
const total = strokes.reduce((m,s)=>Math.max(m, s.isBezier?1500:(s.points?.length ? s.points[s.points.length-1].t : 0)), 0);
const startWall = performance.now();
function step(){
if(!playing) return;
const t = performance.now() - startWall;
strokes.forEach(s=>{
if(s.isBezier){
const A=s.anchors;
const k = Math.min(A.length-1, Math.floor((t/total)*(A.length-1)) );
let d = `M ${A[0].x} ${A[0].y}`;
for(let i=0;i<k;i++){ const a=A[i], b=A[i+1]; d += ` C ${a.h2.x} ${a.h2.y}, ${b.h1.x} ${b.h1.y}, ${b.x} ${b.y}`; }
s.pathEl.setAttribute('d', d);
applyStrokeStyle(s);
s.pathEl.setAttribute('stroke-width', s.width);
}else{
const pts = s.points.filter(p => p.t <= t);
if(pts.length >= 2){
s.pathEl.setAttribute('stroke-width', s.width);
applyStrokeStyle(s);
const smp = s.sampler || samplerSel.value;
s.pathEl.setAttribute('d', smp==='raw'?pathRaw(pts): (smp==='cubic'?pathCubic(pts):pathQuadratic(pts)));
}
}
});
if(t < total) requestAnimationFrame(step);
else { playing=false; markersG.style.opacity = 1; bezierUI.style.opacity = 1; strokes.forEach(updatePath); }
}
requestAnimationFrame(step);
}
function svgToPng(svgString, filename){
const img = new Image();
const svgBlob = new Blob([svgString], {type:'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(svgBlob);
img.onload = function(){
const canvas = document.createElement('canvas');
const vb = stage.viewBox.baseVal;
canvas.width = vb.width; canvas.height = vb.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob)=>{
const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=filename; a.click();
});
URL.revokeObjectURL(url);
};
img.src = url;
}
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,395 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>OneLiner Painter (MultiStroke) draw → export SVG + Motion JSON</title>
<style>
:root{
--bg: #0b0b10; --panel: #12121a; --muted:#8d93a1; --accent:#62d3a4; --accent2:#ffd166; --danger:#ff5c5c;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;display:grid;grid-template-rows:auto 1fr auto;background:var(--bg);color:#e7eaf0;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial}
header,footer{padding:12px 16px;background:var(--panel);border-bottom:1px solid #1e2230}
footer{border-top:1px solid #1e2230;border-bottom:none}
h1{font-size:1.05rem;margin:0}
.wrap{display:grid;grid-template-columns:340px 1fr;gap:16px;padding:16px}
@media (max-width:900px){.wrap{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid #1e2230;border-radius:12px;padding:14px;display:grid;gap:12px}
label{font-size:.9rem;color:#cfd5e3}
input[type="text"],textarea,select{width:100%;padding:10px;border-radius:10px;border:1px solid #2a3042;background:#0f121a;color:#e7eaf0}
input[type="color"]{width:48px;height:36px;border:none;background:none}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.btn{appearance:none;border:none;border-radius:10px;padding:10px 12px;background:#1f2636;color:#e7eaf0;cursor:pointer}
.btn:hover{background:#28314a}
.btn.accent{background:var(--accent);color:#0f131a;font-weight:600}
.btn.warn{background:var(--danger);color:#0f0f10}
.muted{color:var(--muted);font-size:.85rem}
.canvasWrap{position:relative;background:#0f121a;border:1px solid #1e2230;border-radius:12px;overflow:hidden}
svg{display:block;width:100%;height:100%;background:#0f121a}
.kbd{padding:1px 6px;border-radius:8px;background:#1e2333;color:#cfd5e3;font-family:ui-monospace,Menlo,Monaco,monospace}
ul#strokeList{list-style:none;margin:0;padding:0;display:grid;gap:8px;max-height:220px;overflow:auto}
li.strokeItem{display:flex;align-items:center;gap:8px;background:#0f121a;border:1px solid #2a3042;border-radius:10px;padding:8px}
.chip{display:inline-flex;align-items:center;gap:8px;background:#0f121a;border:1px dashed #2a3042;border-radius:10px;padding:6px 10px}
</style>
</head>
<body>
<header>
<h1>OneLiner Painter (MultiStroke) → SVG & Motion JSON • a→Spirale→y • Start/Stop/Dynamik</h1>
</header>
<div class="wrap">
<aside class="panel" aria-labelledby="controlsTitle">
<h2 id="controlsTitle" style="margin:0;font-size:1rem">Werkzeug</h2>
<div class="row">
<label class="chip"><input type="checkbox" id="sparkMode"> <span>Funken setzen (Shift)</span></label>
<label class="chip"><input type="checkbox" id="bitMode"> <span>Bits 1+1 setzen</span></label>
<label class="chip"><input type="checkbox" id="markers"> <span>Start/StopMarker</span></label>
</div>
<div class="row">
<label>Strichstärke
<input type="range" id="stroke" min="4" max="28" step="1" value="12">
</label>
<label>Glättung
<input type="range" id="smooth" min="0" max="10" step="1" value="2">
</label>
</div>
<div class="row">
<label>Farbe A <input type="color" id="colorA" value="#62d3a4"></label>
<label>Farbe B <input type="color" id="colorB" value="#ffd166"></label>
<label>Funken <input type="color" id="colorSpark" value="#e7eaf0"></label>
</div>
<div class="row">
<label class="chip"><input type="radio" name="mode" value="A" checked> <span>StrokeFarbe A</span></label>
<label class="chip"><input type="radio" name="mode" value="B"> <span>StrokeFarbe B</span></label>
<label class="chip"><input type="radio" name="mode" value="GRAD"> <span>Verlauf A→B</span></label>
</div>
<div class="row">
<label class="chip"><input type="checkbox" id="dynWidth"> <span>Breite ~ Geschwindigkeit (JSONonly)</span></label>
<label class="chip"><input type="checkbox" id="captureJSON" checked> <span>Motion JSON mitschreiben</span></label>
</div>
<div>
<strong>Strokes</strong>
<ul id="strokeList" aria-label="StrichListe"></ul>
<p class="muted">Neuer Stroke beginnt mit <span class="kbd">Maus/TouchDown</span>. Ende bei Loslassen. Mehrere Klicks/Wege werden einzeln erfasst.</p>
</div>
<div class="row">
<button class="btn" id="undo">Letzten Stroke löschen</button>
<button class="btn warn" id="clear">Alles löschen</button>
</div>
<div class="row">
<button class="btn" id="copySVG">SVG kopieren</button>
<button class="btn" id="downloadSVG">SVG speichern</button>
<button class="btn accent" id="copyJSON">MotionJSON kopieren</button>
<button class="btn" id="downloadJSON">JSON speichern</button>
</div>
<details>
<summary class="muted">A11y/Meta</summary>
<label>Titel <input id="title" type="text" value="Omi Omega OneLiner"></label>
<label>Beschreibung
<textarea id="desc" rows="3">Cursives kleines a wird zur Spirale; zwei Einsen im a; y hält zusammen; Funken ringsum.</textarea>
</label>
<label>Tag (z.B. Datum/Hashtag) <input id="tag" type="text" value="22.08.25 #CRUMB"></label>
</details>
<p class="muted">Tipps: Ziehen = zeichnen. <span class="kbd">Shift</span> = Funken. <span class="kbd">Z</span> = Undo. <span class="kbd">S</span> speichern.</p>
</aside>
<main class="canvasWrap" aria-label="Zeichenfläche">
<svg id="stage" viewBox="0 0 1200 800" role="img" aria-labelledby="svgTitle svgDesc">
<title id="svgTitle">Omi Omega OneLiner</title>
<desc id="svgDesc">Mehrere Pfade mit Start/Stop und Zeitdynamik: a→Spirale→y; Bits 1+1; Funken.</desc>
<defs>
<linearGradient id="gradA2B" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#62d3a4" />
<stop offset="50%" stop-color="#62d3a4" />
<stop offset="51%" stop-color="#ffd166" />
<stop offset="100%" stop-color="#ffd166" />
</linearGradient>
<style>
.sline{fill:none;stroke-linecap:round;stroke-linejoin:round}
.markerStart{fill:#35c759;stroke:none}
.markerStop{fill:#ff3b30;stroke:none}
</style>
</defs>
<g id="art">
<g id="strokes"></g>
<g id="bits"></g>
<g id="sparks" stroke="#e7eaf0" stroke-width="8" stroke-linecap="round"></g>
<g id="markersG"></g>
<text id="tagText" x="860" y="760" font-family="ui-monospace,monospace" font-size="22" fill="#cfd5e3"></text>
</g>
</svg>
</main>
</div>
<footer>
<span class="muted">Mehrere Klicks & Pfade sind erlaubt. Start/Stop wird als Ereignis erfasst; Geschwindigkeiten landen im MotionJSON.</span>
</footer>
<script>
(function(){
const stage = document.getElementById('stage');
const strokesG= document.getElementById('strokes');
const bitsG = document.getElementById('bits');
const sparksG = document.getElementById('sparks');
const markersG= document.getElementById('markersG');
const grad = document.getElementById('gradA2B');
const tagText = document.getElementById('tagText');
const strokeList = document.getElementById('strokeList');
// Controls
const sparkMode = document.getElementById('sparkMode');
const bitMode = document.getElementById('bitMode');
const markersCb = document.getElementById('markers');
const strokeInp = document.getElementById('stroke');
const smoothInp = document.getElementById('smooth');
const colorAInp = document.getElementById('colorA');
const colorBInp = document.getElementById('colorB');
const colorSInp = document.getElementById('colorSpark');
const dynWidth = document.getElementById('dynWidth');
const captureJSON = document.getElementById('captureJSON');
const titleInp = document.getElementById('title');
const descInp = document.getElementById('desc');
const tagInp = document.getElementById('tag');
const undoBtn = document.getElementById('undo');
const clearBtn = document.getElementById('clear');
const copySVGBtn = document.getElementById('copySVG');
const dlSVGBtn = document.getElementById('downloadSVG');
const copyJSONBtn = document.getElementById('copyJSON');
const dlJSONBtn = document.getElementById('downloadJSON');
let mode = 'A';
document.querySelectorAll('input[name="mode"]').forEach(r=>{
r.addEventListener('change', ()=>{ mode = document.querySelector('input[name="mode"]:checked').value; });
});
// State
let drawing = false;
let curStroke = null; // {id, mode, color, width, points:[{x,y,t}], pathEl}
const strokes = []; // array of strokes
let t0 = null; // session start time
function now(){ return performance.now(); }
function svgPoint(evt){
const pt = stage.createSVGPoint();
pt.x = evt.clientX; pt.y = evt.clientY;
const ctm = stage.getScreenCTM().inverse();
const p = pt.matrixTransform(ctm);
return {x: Math.round(p.x), y: Math.round(p.y)};
}
function updateMeta(){
stage.querySelector('title').textContent = titleInp.value.trim() || 'OneLiner';
stage.querySelector('desc').textContent = descInp.value.trim() || '';
tagText.textContent = tagInp.value.trim();
}
function listRefresh(){
strokeList.innerHTML = '';
strokes.forEach((s,i)=>{
const li = document.createElement('li'); li.className='strokeItem';
const sw = document.createElement('input'); sw.type='range'; sw.min=4; sw.max=28; sw.step=1; sw.value=s.width; sw.title='Breite';
sw.addEventListener('input',()=>{ s.width=+sw.value; s.pathEl.setAttribute('stroke-width', s.width); });
const sel = document.createElement('select');
['A','B','GRAD'].forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; if(s.mode===v) o.selected=true; sel.appendChild(o); });
sel.addEventListener('change',()=>{ s.mode=sel.value; applyStrokeStyle(s); });
const del = document.createElement('button'); del.className='btn'; del.textContent='✕';
del.addEventListener('click',()=>{ removeStroke(i); });
const meta = document.createElement('span'); meta.className='muted';
meta.textContent = `#${s.id}${Math.round(s.duration)}ms • ~${Math.round(s.length)}px`;
li.append('Stroke', sel, sw, del, meta);
strokeList.appendChild(li);
});
}
function removeStroke(idx){
const s = strokes[idx];
if(!s) return;
s.pathEl.remove(); if(s.startMarker) s.startMarker.remove(); if(s.stopMarker) s.stopMarker.remove();
strokes.splice(idx,1);
listRefresh();
}
function createPathEl(){
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('class','sline');
p.setAttribute('stroke-width', strokeInp.value);
strokesG.appendChild(p);
return p;
}
function applyStrokeStyle(s){
if(s.mode==='A'){ s.pathEl.setAttribute('stroke', colorAInp.value); }
else if(s.mode==='B'){ s.pathEl.setAttribute('stroke', colorBInp.value); }
else { s.pathEl.setAttribute('stroke','url(#gradA2B)'); }
}
function toPathD(points){
if(points.length===0) return '';
const sm = +smoothInp.value;
if(points.length<3 || sm===0){
const p0 = points[0];
let d = `M ${p0.x} ${p0.y}`;
for(let i=1;i<points.length;i++){ const p=points[i]; d += ` L ${p.x} ${p.y}`; }
return d;
}
// simple smoothing: use quadratic Beziers between midpoints
let d = `M ${points[0].x} ${points[0].y}`;
for(let i=1;i<points.length-1;i++){
const p0 = points[i];
const p1 = points[i+1];
const mx = (p0.x + p1.x)/2; const my = (p0.y + p1.y)/2;
d += ` Q ${p0.x} ${p0.y} ${mx} ${my}`;
}
const last = points[points.length-1]; d += ` L ${last.x} ${last.y}`;
return d;
}
function startStroke(e){
if(bitMode.checked){ placeBit(e); return; }
if(sparkMode.checked || e.shiftKey){ placeSpark(e); return; }
drawing = true;
if(t0===null) t0 = now();
const t = now() - t0;
const pt = svgPoint(e);
const pathEl = createPathEl();
curStroke = { id: Date.now()%1e7, mode, colorA: colorAInp.value, colorB: colorBInp.value, width:+strokeInp.value, points:[{...pt,t}], pathEl, start:t, duration:0, length:0 };
applyStrokeStyle(curStroke);
updatePath(curStroke);
if(markersCb.checked){ curStroke.startMarker = mark(pt.x, pt.y, 'start'); }
window.addEventListener('pointerup', endStroke, {once:true});
}
function moveStroke(e){
if(!drawing || !curStroke) return;
const p = svgPoint(e);
const t = now() - t0;
const last = curStroke.points[curStroke.points.length-1];
if(!last || Math.hypot(p.x-last.x, p.y-last.y) > 2){
curStroke.points.push({...p,t});
updatePath(curStroke);
}
}
function endStroke(){
if(!curStroke) return;
drawing = false;
const pts = curStroke.points;
curStroke.duration = pts.length? (pts[pts.length-1].t - pts[0].t) : 0;
curStroke.length = polyLen(pts);
if(markersCb.checked){ const last=pts[pts.length-1]; curStroke.stopMarker = mark(last.x,last.y,'stop'); }
strokes.push(curStroke); curStroke = null; listRefresh();
}
function mark(x,y,type){
const r = 7; const c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx',x); c.setAttribute('cy',y); c.setAttribute('r',r);
c.setAttribute('class', type==='start'?'markerStart':'markerStop');
markersG.appendChild(c); return c;
}
function updatePath(s){ s.pathEl.setAttribute('d', toPathD(s.points)); s.pathEl.setAttribute('stroke-width', s.width); }
function polyLen(pts){ let L=0; for(let i=1;i<pts.length;i++){ const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; L += Math.hypot(dx,dy);} return L; }
function placeSpark(e){
const p = svgPoint(e);
const len = Math.max(14, parseInt(strokeInp.value,10)*1.5);
const dx = 10, dy = -10;
const l = document.createElementNS('http://www.w3.org/2000/svg','line');
l.setAttribute('x1', p.x); l.setAttribute('y1', p.y);
l.setAttribute('x2', p.x+dx); l.setAttribute('y2', p.y+dy);
l.setAttribute('stroke', colorSInp.value);
l.setAttribute('stroke-width', Math.max(2, Math.round(parseInt(strokeInp.value,10)*0.66)));
l.setAttribute('stroke-linecap','round');
sparksG.appendChild(l);
}
function placeBit(e){
const p = svgPoint(e);
const h = Math.max(18, parseInt(strokeInp.value,10)*1.2);
const w = Math.max(6, parseInt(strokeInp.value,10)*0.8);
const line1 = document.createElementNS('http://www.w3.org/2000/svg','line');
line1.setAttribute('x1', p.x); line1.setAttribute('y1', p.y-h/2);
line1.setAttribute('x2', p.x); line1.setAttribute('y2', p.y+h/2);
line1.setAttribute('stroke', colorBInp.value);
line1.setAttribute('stroke-width', w);
line1.setAttribute('stroke-linecap', 'round');
bitsG.appendChild(line1);
}
function exportSVGString(){
updateMeta();
const clone = stage.cloneNode(true);
// inline live colors
clone.querySelector('#sparks')?.setAttribute('stroke', colorSInp.value);
// serialize
const s = new XMLSerializer().serializeToString(clone);
return `<?xml version="1.0" encoding="UTF-8"?>
${s}`;
}
function motionJSON(){
const meta = { title:titleInp.value, desc:descInp.value, tag:tagInp.value, t0: t0??0 };
const items = strokes.map(s=>{
// speeds
let maxV=0, sumV=0, nV=0; const pts=s.points;
for(let i=1;i<pts.length;i++){
const dx=pts[i].x-pts[i-1].x, dy=pts[i].y-pts[i-1].y; const dt=(pts[i].t-pts[i-1].t)/1000; if(dt<=0) continue;
const v = Math.hypot(dx,dy)/dt; maxV=Math.max(maxV,v); sumV+=v; nV++;
}
const avgV = nV? sumV/nV : 0;
return {
id: s.id, mode: s.mode, colorA: s.colorA, colorB: s.colorB, width: s.width,
startMs: s.start, durationMs: s.duration, lengthPx: s.length,
avgSpeedPxPerS: +avgV.toFixed(2), maxSpeedPxPerS: +maxV.toFixed(2),
points: s.points.map(p=>({x:p.x,y:p.y,tMs: Math.round(p.t)}))
};
});
return JSON.stringify({meta, strokes:items}, null, 2);
}
// Buttons
document.getElementById('undo').addEventListener('click', ()=>{ if(strokes.length){ removeStroke(strokes.length-1); }});
document.getElementById('clear').addEventListener('click', ()=>{
strokes.length=0; strokesG.innerHTML=''; bitsG.innerHTML=''; sparksG.innerHTML=''; markersG.innerHTML=''; listRefresh(); t0=null; });
copySVGBtn.addEventListener('click', async ()=>{ const svg=exportSVGString(); await navigator.clipboard.writeText(svg); alert('SVG kopiert'); });
dlSVGBtn.addEventListener('click', ()=>{ const svg=exportSVGString(); const blob=new Blob([svg],{type:'image/svg+xml'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega-oneliner_multi.svg'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
copyJSONBtn.addEventListener('click', async ()=>{ const js=motionJSON(); await navigator.clipboard.writeText(js); alert('MotionJSON kopiert'); });
dlJSONBtn.addEventListener('click', ()=>{ const js=motionJSON(); const blob=new Blob([js],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='omi-omega_motion.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1500); });
// Stage events
stage.addEventListener('pointerdown', startStroke);
stage.addEventListener('pointermove', moveStroke);
window.addEventListener('pointerup', endStroke);
// Hotkeys
window.addEventListener('keydown', (e)=>{
if(e.key==='z' || e.key==='Z'){ if(strokes.length){ removeStroke(strokes.length-1); } }
if((e.key==='s' || e.key==='S') && (e.ctrlKey||e.metaKey)){ e.preventDefault(); document.getElementById('downloadSVG').click(); }
});
// Reactive
[titleInp,descInp,tagInp].forEach(inp=>inp.addEventListener('input', updateMeta));
[colorAInp,colorBInp,colorSInp].forEach(inp=>inp.addEventListener('input', ()=>{ tagText.setAttribute('fill','#cfd5e3'); }));
updateMeta();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,722 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>🌈 Rainbow Counter v2 (ohne Anbindung)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Blockly + JS Generator + Deutsche Labels -->
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<style>
:root {
--bg: #1e1e1e;
--panel: #121212;
--txt: #e8e8e8;
--accent: #4caf50;
--muted: #a0a0a0;
--line: #2a2a2a
}
* {
box-sizing: border-box
}
html,
body {
height: 100%;
margin: 0;
background: var(--bg);
color: var(--txt);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace
}
header {
display: flex;
gap: .5rem;
align-items: center;
padding: .6rem .8rem;
border-bottom: 1px solid var(--line)
}
header h2 {
margin: 0;
font-size: 1rem;
font-weight: 700
}
header .sp {
flex: 1
}
button {
padding: .55rem .8rem;
background: var(--accent);
color: #fff;
border: 0;
border-radius: 10px;
font-weight: 700;
cursor: pointer
}
#wrap {
display: flex;
gap: .8rem;
height: calc(100vh - 56px);
padding: .6rem .8rem
}
#left {
flex: 3;
display: flex;
flex-direction: column;
gap: .6rem
}
#right {
flex: 2;
display: flex;
flex-direction: column;
gap: .6rem
}
#blocklyDiv {
height: 60vh;
width: 100%
}
#output {
flex: 1;
min-height: 28vh;
background: var(--panel);
padding: 10px;
border-radius: 10px;
white-space: pre-wrap;
word-break: break-word;
overflow: auto
}
#cfg {
background: var(--panel);
padding: 10px;
border-radius: 10px;
white-space: pre-wrap;
overflow: auto
}
.hint {
color: var(--muted);
font-size: .9rem
}
.row {
display: flex;
gap: .4rem;
flex-wrap: wrap
}
.btn-secondary {
background: #3c99dc
}
.btn-warn {
background: #e67e22
}
</style>
</head>
<body>
<header>
<h2>🧁 Crumbforest • Rainbow Counter v2</h2>
<div class="sp"></div>
<div class="row">
<button id="btnDoc" title="Doku/Beispiel laden">📄 Doku</button>
<button id="btnT1" class="btn-secondary" title="Testfall 1: Basisliste">T1</button>
<button id="btnT2" class="btn-secondary" title="Testfall 2: Gleichstand">T2</button>
<button id="btnT3" class="btn-secondary" title="Testfall 3: Debounce=1">T3</button>
<button id="btnRnd" class="btn-warn" title="Zufalls-Demo laden">🎲 Zufall</button>
<button id="btnClear" title="Leeren">🗑️ Neu</button>
<button id="btnRun" title="Blockly-Code ausführen">▶️ Ausführen</button>
<button id="btnCrew" style="background:#9c27b0" title="Ergebnis an die Crew senden">🚀 An Crew senden</button>
</div>
</header>
<div id="wrap">
<div id="left">
<div id="blocklyDiv"></div>
<pre id="output">🌲 Bereit. Lade „Doku“/„T1T3“ oder baue eigene Logik und drücke ▶️.</pre>
</div>
<div id="right">
<div id="cfg"></div>
<div class="hint">
Ziel-JSON: {"total_events","classes","dominant","mapping"} •
Optional: Debounce (N), Gleichstand-Strategie (per Blocks).
</div>
</div>
</div>
<!-- Minimal-Toolbox -->
<xml id="toolbox" style="display:none">
<category name="Variablen" custom="VARIABLE"></category>
<category name="Logik">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen">
<block type="controls_repeat_ext"></block>
<block type="controls_forEach"></block>
<block type="controls_whileUntil"></block>
</category>
<category name="Mathe">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
</category>
<category name="Listen">
<block type="lists_create_with"></block>
</category>
<category name="Text">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_print"></block>
</category>
<category name="Funktionen" custom="PROCEDURE"></category>
</xml>
<script>
// === CFG: Parameter-Kit (nur Anzeige/Didaktik, kein Netzwerk) ===
const CFG = {
classes: ["red", "green", "blue", "yellow"],
synonyms: { "purple": "unknown", "orange": "unknown" },
normalize: "case_insensitive",
window_n: 12,
debounce_n: 0,
dominant_mode: "max",
tie_policy: "all",
emit_mode: "final_only",
reset_mode: "manual",
include_ts: true,
mapping: { red: "⚠️", green: "✅", blue: "", yellow: "⏳", unknown: "❔" },
rng_seed: 42,
weights: { red: 1, green: 1, blue: 1, yellow: 1, unknown: 0.5 }
};
document.getElementById('cfg').textContent =
'CFG (Gedankenstütze):\\n' + JSON.stringify(CFG, null, 2);
// === Blockly Setup ===
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos',
grid: { spacing: 24, length: 3, colour: '#474747', snap: true },
trashcan: true,
zoom: { startScale: 1.1, maxScale: 2.0, minScale: .6, controls: false, wheel: true },
move: { scrollbars: true, drag: true, wheel: true }
});
// Vordefinierte Variablen (Zähler, Events, Dominant, optional last_ev)
['c_red', 'c_green', 'c_blue', 'c_yellow', 'c_unknown', 'total', 'ev', 'dominant', 'last_ev']
.forEach(v => { try { workspace.createVariable(v); } catch (_) { } });
// Output Helpers
const $ = s => document.querySelector(s);
function setOutput(t) { $('#output').textContent = t }
function appendOutput(t) {
const el = $('#output');
el.textContent += (el.textContent.endsWith('\\n') ? '' : '\\n') + t;
el.scrollTop = el.scrollHeight;
}
window.appendOutput = appendOutput; // für generierten Code
// text_print → Panel (statt alert)
// Wir suchen den Generator (je nach Version unterschiedlich)
const gen = (typeof Blockly.JavaScript !== 'undefined') ? Blockly.JavaScript :
(typeof javascript !== 'undefined' && javascript.javascriptGenerator) ? javascript.javascriptGenerator : null;
if (gen) {
gen['text_print'] = function (block) {
const argument0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || '\'\'';
return 'appendOutput(String(' + argument0 + '));\\n';
};
} else {
console.error("CRUMBBLOCKS: Konnte JavaScript Generator nicht finden!");
}
// XML Parser Fallback
function textToDom(xmlText) {
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch (_) { }
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
}
function loadXml(xml) { const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
// === DOKU XML ===
const DOC_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>c_red</variable><variable>c_green</variable><variable>c_blue</variable>
<variable>c_yellow</variable><variable>c_unknown</variable><variable>total</variable>
<variable>ev</variable><variable>dominant</variable>
</variables>
<!-- DOKU -->
<block type="text_print" x="20" y="20">
<value name="TEXT"><block type="text"><field name="TEXT">DOC: Regenbogen-Zähl-Maschine zählt Farbereignisse und erzeugt JSON-Report.</field></block></value>
<next><block type="text_print"><value name="TEXT"><block type="text"><field name="TEXT">Eingabe (Demo): ["red","green","blue","blue","yellow","purple"]</field></block></value></block></next>
</block>
<block type="text_print" x="20" y="88">
<value name="TEXT"><block type="text"><field name="TEXT">Erwartet: {"total_events","classes":{"red","green","blue","yellow","unknown"},"dominant","mapping"}</field></block></value>
</block>
<!-- Init -->
<block type="variables_set" x="20" y="150">
<field name="VAR">c_red</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next><block type="variables_set"><field name="VAR">c_green</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_blue</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_yellow</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_unknown</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">total</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
</next></next></next></next></next>
</block>
<!-- Demo-Ereignisse -->
<block type="controls_forEach" x="20" y="320">
<field name="VAR">ev</field>
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">red</field></block></value>
<value name="ADD1"><block type="text"><field name="TEXT">green</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">blue</field></block></value>
<value name="ADD3"><block type="text"><field name="TEXT">blue</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">yellow</field></block></value>
<value name="ADD5"><block type="text"><field name="TEXT">purple</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set">
<field name="VAR">total</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
<next>
<block type="controls_if">
<mutation elseif="3" else="1"></mutation>
<value name="IF0"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">red</field></block></value>
</block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">c_red</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
</block></statement>
<value name="IF1"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">green</field></block></value>
</block></value>
<statement name="DO1"><block type="variables_set"><field name="VAR">c_green</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
</block></statement>
<value name="IF2"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">blue</field></block></value>
</block></value>
<statement name="DO2"><block type="variables_set"><field name="VAR">c_blue</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
</block></statement>
<value name="IF3"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">yellow</field></block></value>
</block></value>
<statement name="DO3"><block type="variables_set"><field name="VAR">c_yellow</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
</block></statement>
<statement name="ELSE"><block type="variables_set"><field name="VAR">c_unknown</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_unknown</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
</block></statement>
</block>
</next>
</block>
</statement>
</block>
<!-- Dominant (einfach) -->
<block type="variables_set" x="20" y="610">
<field name="VAR">dominant</field>
<value name="VALUE"><block type="text"><field name="TEXT">blue</field></block></value>
</block>
<!-- JSON Report -->
<block type="text_print" x="20" y="650">
<value name="TEXT">
<block type="text_join">
<mutation items="22"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "total_events": </field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "classes": { "red": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "green": </field></block></value>
<value name="ADD5"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="ADD6"><block type="text"><field name="TEXT">, "blue": </field></block></value>
<value name="ADD7"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="ADD8"><block type="text"><field name="TEXT">, "yellow": </field></block></value>
<value name="ADD9"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="ADD10"><block type="text"><field name="TEXT">, "unknown": </field></block></value>
<value name="ADD11"><block type="variables_get"><field name="VAR">c_unknown</field></block></value>
<value name="ADD12"><block type="text"><field name="TEXT"> }, "dominant": "</field></block></value>
<value name="ADD13"><block type="variables_get"><field name="VAR">dominant</field></block></value>
<value name="ADD14"><block type="text"><field name="TEXT">", "mapping": { "red": "⚠️", "green": "✅", "blue": "", "yellow": "⏳", "unknown": "❔" } }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === Testfälle ===
const T1_XML = DOC_XML; // Basisliste, dominant=blue (einfach)
const T2_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>c_red</variable><variable>c_green</variable><variable>c_blue</variable>
<variable>c_yellow</variable><variable>c_unknown</variable><variable>total</variable>
<variable>ev</variable><variable>dominant</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">T2: Gleichstand red vs blue</field></block></value></block>
<!-- Init -->
<block type="variables_set" x="20" y="70"><field name="VAR">c_red</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next><block type="variables_set"><field name="VAR">c_green</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_blue</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_yellow</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_unknown</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">total</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
</next></next></next></next></next>
</block>
<!-- Events: ["red","blue","red","blue"] -->
<block type="controls_forEach" x="20" y="230">
<field name="VAR">ev</field>
<value name="LIST"><block type="lists_create_with"><mutation items="4"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">red</field></block></value>
<value name="ADD1"><block type="text"><field name="TEXT">blue</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">red</field></block></value>
<value name="ADD3"><block type="text"><field name="TEXT">blue</field></block></value>
</block></value>
<statement name="DO">
<block type="variables_set">
<field name="VAR">total</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
<next>
<block type="controls_if">
<mutation elseif="1" else="1"></mutation>
<value name="IF0"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">red</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">c_red</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF1"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">blue</field></block></value></block></value>
<statement name="DO1"><block type="variables_set"><field name="VAR">c_blue</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<statement name="ELSE"><block type="text_print"><value name="TEXT"><block type="text"><field name="TEXT">Ignoriere andere.</field></block></value></block></statement>
</block>
</next>
</block>
</statement>
</block>
<!-- Gleichstand: dominant = "red,blue" (Liste als Text) -->
<block type="variables_set" x="20" y="470"><field name="VAR">dominant</field>
<value name="VALUE"><block type="text"><field name="TEXT">["red","blue"]</field></block></value>
</block>
<!-- JSON Report -->
<block type="text_print" x="20" y="520">
<value name="TEXT">
<block type="text_join">
<mutation items="24"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "total_events": </field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "classes": { "red": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "green": </field></block></value>
<value name="ADD5"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="ADD6"><block type="text"><field name="TEXT">, "blue": </field></block></value>
<value name="ADD7"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="ADD8"><block type="text"><field name="TEXT">, "yellow": </field></block></value>
<value name="ADD9"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="ADD10"><block type="text"><field name="TEXT">, "unknown": </field></block></value>
<value name="ADD11"><block type="variables_get"><field name="VAR">c_unknown</field></block></value>
<value name="ADD12"><block type="text"><field name="TEXT"> }, "dominant_all": </field></block></value>
<value name="ADD13"><block type="variables_get"><field name="VAR">dominant</field></block></value>
<value name="ADD14"><block type="text"><field name="TEXT">, "mapping": { "red": "⚠️", "green": "✅", "blue": "", "yellow": "⏳", "unknown": "❔" } }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
const T3_XML = `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable>c_red</variable><variable>c_green</variable><variable>c_blue</variable>
<variable>c_yellow</variable><variable>c_unknown</variable><variable>total</variable>
<variable>ev</variable><variable>dominant</variable><variable>last_ev</variable>
</variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">T3: Debounce 1 (identische Wiederholung wird ignoriert)</field></block></value></block>
<!-- Init -->
<block type="variables_set" x="20" y="70"><field name="VAR">c_red</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next><block type="variables_set"><field name="VAR">c_green</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_blue</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_yellow</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_unknown</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">total</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
</next></next></next></next></next>
</block>
<block type="variables_set" x="20" y="230"><field name="VAR">last_ev</field><value name="VALUE"><block type="text"><field name="TEXT">_none_</field></block></value></block>
<!-- Events: ["blue","blue","green"] -->
<block type="controls_forEach" x="20" y="280">
<field name="VAR">ev</field>
<value name="LIST"><block type="lists_create_with"><mutation items="3"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">blue</field></block></value>
<value name="ADD1"><block type="text"><field name="TEXT">blue</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">green</field></block></value>
</block></value>
<statement name="DO">
<block type="controls_if">
<value name="IF0"><block type="logic_compare"><field name="OP">NEQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">last_ev</field></block></value>
</block></value>
<statement name="DO0">
<block type="variables_set">
<field name="VAR">total</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
<next>
<block type="controls_if">
<mutation elseif="3" else="1"></mutation>
<value name="IF0"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">red</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">c_red</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF1"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">green</field></block></value></block></value>
<statement name="DO1"><block type="variables_set"><field name="VAR">c_green</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF2"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">blue</field></block></value></block></value>
<statement name="DO2"><block type="variables_set"><field name="VAR">c_blue</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF3"><block type="logic_compare"><field name="OP">EQ</field>
<value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value>
<value name="B"><block type="text"><field name="TEXT">yellow</field></block></value></block></value>
<statement name="DO3"><block type="variables_set"><field name="VAR">c_yellow</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<statement name="ELSE"><block type="variables_set"><field name="VAR">c_unknown</field>
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">c_unknown</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
</block>
</next>
</block>
</statement>
</block>
<next><block type="variables_set"><field name="VAR">last_ev</field><value name="VALUE"><block type="variables_get"><field name="VAR">ev</field></block></value></block></next>
</statement>
</block>
<!-- Dominant (einfach max-Entscheid) -->
<block type="variables_set" x="20" y="620"><field name="VAR">dominant</field>
<value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value>
</block>
<block type="controls_if" x="20" y="660">
<mutation elseif="3" else="1"></mutation>
<value name="IF0"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_red</field></block></value>
</block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">blue</field></block></value></block></statement>
<value name="IF1"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
</block></value>
<statement name="DO1"><block type="variables_set"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">red</field></block></value></block></statement>
<value name="IF2"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
</block></value>
<statement name="DO2"><block type="variables_set"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">green</field></block></value></block></statement>
<value name="IF3"><block type="logic_compare"><field name="OP">GT</field>
<value name="A"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="B"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
</block></value>
<statement name="DO3"><block type="variables_set"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">yellow</field></block></value></block></statement>
<statement name="ELSE"><block type="variables_set"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">blue</field></block></value></block></statement>
</block>
<!-- JSON -->
<block type="text_print" x="20" y="830">
<value name="TEXT">
<block type="text_join">
<mutation items="22"></mutation>
<value name="ADD0"><block type="text"><field name="TEXT">{ "total_events": </field></block></value>
<value name="ADD1"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="ADD2"><block type="text"><field name="TEXT">, "classes": { "red": </field></block></value>
<value name="ADD3"><block type="variables_get"><field name="VAR">c_red</field></block></value>
<value name="ADD4"><block type="text"><field name="TEXT">, "green": </field></block></value>
<value name="ADD5"><block type="variables_get"><field name="VAR">c_green</field></block></value>
<value name="ADD6"><block type="text"><field name="TEXT">, "blue": </field></block></value>
<value name="ADD7"><block type="variables_get"><field name="VAR">c_blue</field></block></value>
<value name="ADD8"><block type="text"><field name="TEXT">, "yellow": </field></block></value>
<value name="ADD9"><block type="variables_get"><field name="VAR">c_yellow</field></block></value>
<value name="ADD10"><block type="text"><field name="TEXT">, "unknown": </field></block></value>
<value name="ADD11"><block type="variables_get"><field name="VAR">c_unknown</field></block></value>
<value name="ADD12"><block type="text"><field name="TEXT"> }, "dominant": "</field></block></value>
<value name="ADD13"><block type="variables_get"><field name="VAR">dominant</field></block></value>
<value name="ADD14"><block type="text"><field name="TEXT">", "mapping": { "red": "⚠️", "green": "✅", "blue": "", "yellow": "⏳", "unknown": "❔" } }</field></block></value>
</block>
</value>
</block>
</xml>
`.trim();
// === Zufalls-Demo-Generator (nur Vorstellung, keine Sensorik) ===
function randomDemoXml(n = CFG.window_n) {
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange'];
// einfacher RNG (konstant)
let seed = CFG.rng_seed;
function rnd() { seed = (seed * 1664525 + 1013904223) % 4294967296; return seed / 4294967296; }
const arr = Array.from({ length: n }, () => colors[Math.floor(rnd() * colors.length)]);
const items = arr.map((c, i) => `
<value name="ADD${i}"><block type="text"><field name="TEXT">${c}</field></block></value>`).join('');
return `
<xml xmlns="https://developers.google.com/blockly/xml">
<variables><variable>ev</variable><variable>total</variable><variable>c_red</variable><variable>c_green</variable><variable>c_blue</variable><variable>c_yellow</variable><variable>c_unknown</variable><variable>dominant</variable></variables>
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Zufalls-Ereignisse: ${arr.join(', ')}</field></block></value></block>
<block type="variables_set" x="20" y="80"><field name="VAR">c_red</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
<next><block type="variables_set"><field name="VAR">c_green</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_blue</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_yellow</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">c_unknown</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
<block type="variables_set"><field name="VAR">total</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value>
</next></next></next></next></next>
</block>
<block type="controls_forEach" x="20" y="260">
<field name="VAR">ev</field>
<value name="LIST"><block type="lists_create_with"><mutation items="${n}"></mutation>${items}</block></value>
<statement name="DO">
<block type="variables_set"><field name="VAR">total</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
<value name="A"><block type="variables_get"><field name="VAR">total</field></block></value>
<value name="B"><block type="math_number"><field name="NUM">1</field></block></value>
</block></value>
<next>
<block type="controls_if"><mutation elseif="3" else="1"></mutation>
<value name="IF0"><block type="logic_compare"><field name="OP">EQ</field><value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value><value name="B"><block type="text"><field name="TEXT">red</field></block></value></block></value>
<statement name="DO0"><block type="variables_set"><field name="VAR">c_red</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">c_red</field></block></value><value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF1"><block type="logic_compare"><field name="OP">EQ</field><value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value><value name="B"><block type="text"><field name="TEXT">green</field></block></value></block></value>
<statement name="DO1"><block type="variables_set"><field name="VAR">c_green</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">c_green</field></block></value><value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF2"><block type="logic_compare"><field name="OP">EQ</field><value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value><value name="B"><block type="text"><field name="TEXT">blue</field></block></value></block></value>
<statement name="DO2"><block type="variables_set"><field name="VAR">c_blue</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">c_blue</field></block></value><value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<value name="IF3"><block type="logic_compare"><field name="OP">EQ</field><value name="A"><block type="variables_get"><field name="VAR">ev</field></block></value><value name="B"><block type="text"><field name="TEXT">yellow</field></block></value></block></value>
<statement name="DO3"><block type="variables_set"><field name="VAR">c_yellow</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">c_yellow</field></block></value><value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
<statement name="ELSE"><block type="variables_set"><field name="VAR">c_unknown</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">c_unknown</field></block></value><value name="B"><block type="math_number"><field name="NUM">1</field></block></value></block></value></block></statement>
</block>
</next>
</statement>
</block>
<block type="variables_set" x="20" y="560"><field name="VAR">dominant</field><value name="VALUE"><block type="text"><field name="TEXT">blue</field></block></value></block>
<block type="text_print" x="20" y="600"><value name="TEXT"><block type="text"><field name="TEXT">→ Ergänze IF-Kette oder eigene Funktion für „dominant“.</field></block></value></block>
</xml>
`.trim();
}
// === Actions ===
document.getElementById('btnDoc').onclick = () => { loadXml(DOC_XML); setOutput('📄 Doku geladen. Lies die DOC-Zeilen und drücke ▶️.'); };
document.getElementById('btnT1').onclick = () => { loadXml(T1_XML); setOutput('🧪 T1 geladen (Basisliste). ▶️ für Report.'); };
document.getElementById('btnT2').onclick = () => { loadXml(T2_XML); setOutput('🧪 T2 geladen (Gleichstand). ▶️ für Report mit dominant_all=["red","blue"].'); };
document.getElementById('btnT3').onclick = () => { loadXml(T3_XML); setOutput('🧪 T3 geladen (Debounce=1). ▶️ für Report.'); };
document.getElementById('btnRnd').onclick = () => { loadXml(randomDemoXml()); setOutput('🎲 Zufalls-Demo geladen. Ergänze Dominanz-Logik & ▶️.'); };
document.getElementById('btnClear').onclick = () => { workspace.clear(); setOutput('🧹 Workspace geleert.'); };
document.getElementById('btnRun').onclick = () => {
const gen = (typeof Blockly.JavaScript !== 'undefined') ? Blockly.JavaScript :
(typeof javascript !== 'undefined' && javascript.javascriptGenerator) ? javascript.javascriptGenerator : null;
if (!gen) { alert("Fehler: Generator nicht geladen."); return; }
const code = gen.workspaceToCode(workspace);
// Safety: Falls der Generator doch alert() nutzt, fangen wir das ab.
const oldAlert = window.alert;
window.alert = function (msg) { appendOutput(String(msg)); };
try {
new Function(code)();
appendOutput('\\n✅ Fertig.');
}
catch (e) { appendOutput('\\n❌ Fehler: ' + e); }
finally {
window.alert = oldAlert; // Restore
}
};
document.getElementById('btnCrew').onclick = async () => {
const outTxt = document.getElementById('output').textContent;
// Try to find the JSON part
const jsonMatch = outTxt.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
await navigator.clipboard.writeText(jsonMatch[0]);
alert('🚀 Daten kopiert!\n\nGehe jetzt in dein Terminal und führe aus:\n./missions/evaluate_mission_data.sh\n\n(Dort kannst du die Daten dann einfügen)');
} catch (err) {
alert('❌ Konnte nicht in die Zwischenablage kopieren: ' + err);
}
} else {
alert('⚠️ Kein gültiges JSON-Ergebnis gefunden!\nBitte führe erst den Code aus (▶️).');
}
};
</script>
</body>
</html>

31
crumbblocks/readme.txt Normal file
View File

@@ -0,0 +1,31 @@
# Schnippsi Painter — Streaming Quickstart
## 1) Datei
Öffne `schnippsi_painter_stream.html` im Browser. Zeichne frei oder mit dem BezierTool.
- `Verbinden` stellt eine WebSocket-Verbindung zu **ws://localhost:9980** her.
- Alle Events werden als JSON gesendet:
- `session` — Sessionstart
- `strokeStart` — neues Stroke (tool, colors, width)
- `point` — Punkte beim Freihandzeichnen (x,y,t,pressure,tilt)
- `anchor` / `anchors` — Bezier-Anker & Griffe (inkl. h1/h2)
- `strokeEnd` — Stroke abgeschlossen
## 2) TouchDesigner (ohne extra Code)
- Füge **WebSocket DAT** hinzu, setze **Mode = Server** und **Port = 9980**.
- Verbinde Browser (Button `Verbinden`).
- Hänge einen **JSON DAT** an den WebSocket-Ausgang (zum Parsen).
- Alternativ direkt WebSocket DAT -> **WebSocket In CHOP** (vereinfachte Skalare), für volle Struktur aber JSON DAT + Python DAT-Execute.
- Beispiel: Aus `point`-Messages `x,y` in einen **Trail CHOP** oder zum Zeichnen in einen **TOP**.
## 3) Test ohne TouchDesigner (Node)
Optionale MiniWS:
```bash
npm i ws
node mock_ws_server.js
```
Dann im Painter `ws://localhost:9980` verwenden.
## 4) Export
Buttons: JSON, SVG, PNG — jeweils Download.
A11y: Tastaturbedienung, ARIAStatus, hoher Kontrast, 200% Zoom stabil.

293
crumbblocks/schnippsi_ui.html Executable file
View File

@@ -0,0 +1,293 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dein Zeichen im Wald 🌿</title>
<style>
/* ✂️ SCHNIPPSI'S CSS LABOR ✂️ */
:root {
--bg-color: #121212;
--text-color: #e0e0e0;
--glass-bg: rgba(255, 255, 255, 0.1);
--glass-border: rgba(255, 255, 255, 0.2);
--accent: #4caf50;
/* Waldgrün */
--accent-hover: #66bb6a;
--shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-color);
color: var(--text-color);
/* Zentrierung: Das Herz des Layouts */
display: flex;
justify-content: center;
align-items: center;
/* Ein bisschen Wald-Atmosphäre */
background-image: radial-gradient(circle at 50% 50%, #1e2820 0%, #0a0e0b 100%);
}
/* Der Container für dein Zeichen */
.forest-container {
text-align: center;
}
/* Das "Zeichen" (Der Button/Trigger) */
.sign-box {
background: var(--glass-bg);
backdrop-filter: blur(10px);
/* Der Glassmorphism Effekt */
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 40px 60px;
box-shadow: var(--shadow);
cursor: pointer;
transition: transform 0.3s ease, border-color 0.3s ease;
}
.sign-box:hover {
transform: translateY(-5px);
border-color: var(--accent);
}
.sign-icon {
font-size: 4rem;
margin-bottom: 10px;
display: block;
}
.sign-label {
font-size: 1.5rem;
font-weight: bold;
letter-spacing: 1px;
}
/* 🌶️ DAS MODAL (Overlay) */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
pointer-events: none;
/* Klick durchlassen wenn unsichtbar */
transition: opacity 0.3s ease;
z-index: 100;
}
.modal-overlay.active {
opacity: 1;
pointer-events: auto;
}
.modal-content {
background: #1e1e1e;
border: 1px solid var(--accent);
border-radius: 15px;
padding: 30px;
width: 90%;
max-width: 400px;
box-shadow: 0 0 20px rgba(76, 175, 80, 0.2);
transform: scale(0.9);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.modal-overlay.active .modal-content {
transform: scale(1);
}
h3 {
margin-top: 0;
color: var(--accent);
}
label {
display: block;
margin: 15px 0 5px;
font-size: 0.9rem;
color: #aaa;
text-align: left;
}
input,
textarea {
width: 100%;
padding: 10px;
background: #2a2a2a;
border: 1px solid #444;
border-radius: 5px;
color: white;
font-family: inherit;
}
textarea {
height: 100px;
resize: none;
}
input:focus,
textarea:focus {
outline: none;
border-color: var(--accent);
}
.actions {
margin-top: 20px;
display: flex;
justify-content: space-between;
}
button {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
transition: opacity 0.2s;
}
.btn-cancel {
background: transparent;
color: #aaa;
border: 1px solid #444;
}
.btn-cancel:hover {
color: white;
border-color: white;
}
.btn-send {
background: var(--accent);
color: white;
}
.btn-submit:hover {
opacity: 0.9;
}
/* Nachricht nach dem Senden */
#status {
position: absolute;
bottom: 20px;
font-size: 0.9rem;
color: var(--accent);
opacity: 0;
transition: opacity 0.5s;
}
</style>
</head>
<body>
<!-- 🏛️ TEMPLATUS DOM STRUKTUR -->
<div class="forest-container">
<div class="sign-box" id="openModalBtn">
<span class="sign-icon">🌲</span>
<span class="sign-label">ZEICHEN SETZEN</span>
</div>
<div id="status">Daten kopiert! Zurück zum Terminal.</div>
</div>
<!-- MODAL -->
<div class="modal-overlay" id="modal">
<div class="modal-content">
<h3>Dein Zeichen im Wald</h3>
<p>Hinterlasse eine Nachricht für die anderen Krümel.</p>
<label for="inpName">Dein Name (oder Pseudonym)</label>
<input type="text" id="inpName" placeholder="z.B. Wanderer">
<label for="inpMsg">Deine Botschaft</label>
<textarea id="inpMsg" placeholder="Was hast du entdeckt?"></textarea>
<div class="actions">
<button class="btn-cancel" id="closeModalBtn">Abbrechen</button>
<button class="btn-send" id="sendBtn">🚀 Senden</button>
</div>
</div>
</div>
<script>
// 🌶️ PEPPER'S LOGIK LABOR
const modal = document.getElementById('modal');
const btnOpen = document.getElementById('openModalBtn');
const btnClose = document.getElementById('closeModalBtn');
const btnSend = document.getElementById('sendBtn');
const inpName = document.getElementById('inpName');
const inpMsg = document.getElementById('inpMsg');
const status = document.getElementById('status');
// Modal öffnen
btnOpen.addEventListener('click', () => {
modal.classList.add('active');
inpName.focus();
});
// Modal schließen
btnClose.addEventListener('click', () => {
modal.classList.remove('active');
});
// Klick auf Hintergrund schließt auch
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.classList.remove('active');
});
// Senden & JSON bauen
btnSend.addEventListener('click', async () => {
const name = inpName.value.trim() || 'Unbekannt';
const msg = inpMsg.value.trim() || 'Stilles Rauschen...';
// Das Datenpaket für die Crew
const data = {
mission: "schnippsi_ui",
author: name,
message: msg,
timestamp: new Date().toISOString(),
style: "glassmorphism"
};
const jsonStr = JSON.stringify(data, null, 2);
try {
await navigator.clipboard.writeText(jsonStr);
// UI Feedback
modal.classList.remove('active');
status.textContent = "✨ Copied! Füge es jetzt im Terminal ein: ./missions/evaluate_sign.sh";
status.style.opacity = '1';
// Nach 3s ausblenden
setTimeout(() => { status.style.opacity = '0'; }, 5000);
} catch (err) {
alert("Fehler beim Kopieren: " + err);
}
});
</script>
</body>
</html>

View File

@@ -5,13 +5,14 @@ API_KEY="$OPENROUTER_API_KEY"
MODEL="openai/gpt-3.5-turbo"
# Verzeichnisse
LOG_DIR="/home/zero/.templatus_logs"
LOG_DIR="${CRUMB_LOGS_DIR:-$HOME/.crumbforest_logs}/templatus"
HISTORY_FILE="$LOG_DIR/templatus_history.json"
TOKEN_LOG="$LOG_DIR/token_log.json"
TMP_REQUEST="/tmp/templatus_request.json"
TMP_RESPONSE="/tmp/templatus_response.json"
mkdir -p "$LOG_DIR"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
# JSON Payload vorbereiten
cat <<EOF > "$TMP_REQUEST"

View File

@@ -0,0 +1,8 @@
{
"title": "Dein Zeichen im Wald",
"icon": "🎨",
"description": "Erstelle ein UI-Blatt mit HTML & CSS und hinterlasse eine Nachricht.",
"difficulty": "medium",
"author": "Schnippsi",
"enabled": true
}

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# 🖌️ Design-Challenge: Dein Zeichen im Wald
# Eine Mission über Struktur, Stil und Gefühl.
# Waldwächter laden
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Relativer Pfad zur Lib, da wir in missions/challenges/ sind (2 Ebenen tiefer)
LIB_PATH="${SCRIPT_DIR}/../../lib/waldwaechter.sh"
if [ -f "$LIB_PATH" ]; then
source "$LIB_PATH"
else
# Fallback für direkte Ausführung
echo "⚠️ Waldwächter Lib nicht gefunden. Nutze Standard-Ausgabe."
function templatus() { echo "🏛️ Templatus: $1"; }
function schnippsi() { echo "✂️ Schnippsi: $1"; }
function pepperphp() { echo "🌶️ Pepper: $1"; }
fi
clear
cat << "EOF"
🎨 DEIN ZEICHEN IM WALD 🎨
Eine Mission für HTML, CSS und das gute Gefühl.
EOF
echo ""
sleep 1
echo "🏛️ Templatus betritt die Lichtung..."
echo ""
templatus "Sei gegrüßt, Architekt. Der Wald braucht Struktur. Ein Baum ist nicht nur Holz, er ist ein Gerüst (DOM). Wir brauchen ein stabiles Fundament."
templatus "Deine Aufgabe: Erstelle ein HTML-Dokument. Ein 'Blatt' im Wind des Browsers."
echo ""
read -p " (Drücke ENTER um das Gerüst zu bauen...)"
echo ""
echo "✂️ Schnippsi schwebt herein..."
echo ""
schnippsi "Struktur? Pfff, Templatus, du bist so trocken wie altes Papier! Ein Blatt muss LEBEN!"
schnippsi "Es braucht Farbe, Licht und Schatten. Wir wollen Glassmorphism durchscheinend wie Tau auf einem Blatt. Und es muss auf jedem Gerät gut aussehen (Responsive). Das ist Ästhetik!"
echo ""
read -p " (Drücke ENTER für den Style...)"
echo ""
echo "🌶️ PepperPHP flitzt vorbei..."
echo ""
pepperphp "Und es muss was TUN! Ein statisches Blatt ist langweilig. Wenn man draufklickt, muss ein MODAL aufgehen! 'onClick', 'classList.add', Action! Wir wollen eine Nachricht hinterlassen."
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📋 DEINE MISSION:"
echo "1. Öffne im Browser: crumbblocks/schnippsi_ui.html"
echo "2. Betrachte das Werk. Es ist... noch leer (oder?)"
echo "3. Dein Ziel: Eine mittige Box ('Das Zeichen')."
echo "4. Klick -> Modal -> Nachricht eingeben -> Senden."
echo "5. Das Ergebnis bringst du zurück zur Crew."
echo ""
read -p "🚀 Bereit zum Coden? (j/n) " READY
if [[ "$READY" == "j" ]]; then
echo ""
echo "Dann los! Starte den Server:"
echo "./start_crumbblocks.sh"
echo ""
echo "Und bearbeite die Datei: crumbblocks/schnippsi_ui.html"
else
echo "Lass dir Zeit. Gutes Design braucht Muße."
fi

133
missions/evaluate_mission_data.sh Executable file
View File

@@ -0,0 +1,133 @@
#!/bin/bash
# 🚀 Mission Data Evaluator (Master Gateway)
# Liest JSON vom Browser und routet zur richtigen Mission.
# Waldwächter laden
# Waldwächter laden
MISSION_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${MISSION_DIR}/../lib/waldwaechter.sh"
clear
cat << "EOF"
📡 CRUMB-MISSION DATA LINK 📡
Verbindung zum Browser wird hergestellt...
Anleitung:
1. Öffne die Crumbblocks Mission im Browser
2. Baue deinen Code und klicke "▶️ Ausführen"
3. Klicke "🚀 An Crew Senden"
4. Füge den kopierten Code hier ein (Ctrl+V / Cmd+V)
5. Drücke ENTER und dann CTRL+D (um das Einfügen beenden)
EOF
echo ""
echo "👇 Bitte JSON-Daten jetzt einfügen:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Lese Input bis EOF
INPUT_DATA=$(cat)
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔄 Daten empfangen. Prüfe Mission-Typ..."
sleep 0.5
# Extrahiere Mission-ID aus dem JSON
# Wir nutzen grep/cut als robusten Fallback, falls jq fehlt
MISSION_ID=$(echo "$INPUT_DATA" | grep -o '"mission": *"[^"]*"' | cut -d'"' -f4)
# Routing Logic
if [[ "$MISSION_ID" == "schnippsi_ui" ]]; then
# --> Rerouting zur UI Mission
echo ">> Routing zu: Dein Zeichen im Wald (Schnippsi UI)"
echo "$INPUT_DATA" | "${MISSION_DIR}/evaluate_sign.sh"
exit $?
fi
# ============================================================
# DEFAULT / LEGACY LOGIC (Rainbow Counter)
# ============================================================
# Falls keine Mission-ID gefunden wurde oder unbekannt, gehen wir davon aus,
# dass es der Rainbow Counter ist (Backward Compatibility).
echo ">> Routing zu: Standard (Rainbow Counter)"
echo ""
# Validierung
if [[ "$INPUT_DATA" != *"{"* ]] || [[ "$INPUT_DATA" != *"}"* ]]; then
echo "❌ FEHLER: Das sieht nicht wie gültiges JSON aus."
exit 1
fi
has_jq=$(command -v jq)
# Versuch, die relevante Zeile zu finden (total_events)
VALID_JSON_LINE=$(echo "$INPUT_DATA" | grep '"total_events":' | head -n 1)
if [ -z "$VALID_JSON_LINE" ]; then
CLEAN_JSON="$INPUT_DATA"
else
CLEAN_JSON="$VALID_JSON_LINE"
fi
CLEAN_JSON=$(echo "$CLEAN_JSON" | sed 's/^[^{]*{/{/; s/}[^}]*$/}/')
if [ -n "$has_jq" ] && echo "$CLEAN_JSON" | jq . >/dev/null 2>&1; then
TOTAL=$(echo "$CLEAN_JSON" | jq -r '.total_events // 0')
DOMINANT=$(echo "$CLEAN_JSON" | jq -r '.dominant // "unknown"')
RED=$(echo "$CLEAN_JSON" | jq -r '.classes.red // 0')
BLUE=$(echo "$CLEAN_JSON" | jq -r '.classes.blue // 0')
else
# Fallback Parser
TOTAL=$(echo "$CLEAN_JSON" | grep -o '"total_events": *[0-9]*' | awk -F: '{print $2}' | tr -d ' ,')
DOMINANT=$(echo "$INPUT_DATA" | grep -o '"dominant": *"[^"]*"' | awk -F: '{print $2}' | tr -d ' "')
RED=$(echo "$INPUT_DATA" | grep -o '"red": *[0-9]*' | awk -F: '{print $2}' | tr -d ' ,')
BLUE=$(echo "$INPUT_DATA" | grep -o '"blue": *[0-9]*' | awk -F: '{print $2}' | tr -d ' ,')
fi
# Default Werte falls leer
TOTAL=${TOTAL:-0}
RED=${RED:-0}
BLUE=${BLUE:-0}
DOMINANT=${DOMINANT:-unknown}
# Feedback der Crew
echo "🐘 DumboSQL prüft die Struktur..."
sleep 1
dumbosql "Datensatz empfangen. $TOTAL Ereignisse gefunden. Die Syntax ist valide. Speichere in temporärem Cache..."
echo ""
echo "🦊 FunkFox analysiert den Flow..."
sleep 1
if [ "$TOTAL" -gt 10 ]; then
funkfox "Wow, da ist ordentlich was los! $TOTAL Signale verarbeitet. Der Flow ist fast schon ein Stream!"
elif [ "$TOTAL" -eq 0 ]; then
funkfox "Äh, Stille? Ich höre nichts. Null Events. Sicher, dass der Code lief?"
else
funkfox "Okay, $TOTAL Signale. Ein guter Start für einen kleinen Loop."
fi
echo ""
echo "🦉 Maya-Eule betrachtet die Farben..."
sleep 1
case $DOMINANT in
"red") mayaeule "Rot dominiert. Energie und Warnung." ;;
"blue") mayaeule "Blau ist stark. Ruhe und Technik." ;;
"green") mayaeule "Grün wie der Wald. Alles im Bereich." ;;
"yellow") mayaeule "Gelb leuchtet wie die Sonne." ;;
*) mayaeule "Eine interessante Mischung ($DOMINANT). Vielfalt ist gut." ;;
esac
echo ""
echo "📊 Statistik:"
echo " 🔴 Rot: $RED"
echo " 🔵 Blau: $BLUE"
echo ""
cat << "EOF"
✅ ANALYSE ABGESCHLOSSEN
Die Crew bestätigt: Dein Blockly-Code funktioniert!
EOF

60
missions/evaluate_sign.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# 🌿 Auswertung: Dein Zeichen im Wald
# Waldwächter laden
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
clear
cat << "EOF"
🌿 WALD-LOGBUCH EMPFÄNGER 🌿
Bitte füge dein "Zeichen" (JSON) aus dem Browser ein.
(Drücke danach ENTER und CTRL+D)
EOF
echo ""
echo "👇 DATA DROP:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Input lesen mit sed trick um nur valid JSON zu finden (wie bei evaluate_mission_data)
INPUT_DATA=$(cat)
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔄 Analysiere Ästhetik und Inhalt..."
sleep 1
echo ""
# JSON Extraction (Simple Grep/Sed Fallback)
# Wir suchen nach "author" und "message"
AUTHOR=$(echo "$INPUT_DATA" | grep -o '"author": *"[^"]*"' | cut -d'"' -f4)
MESSAGE=$(echo "$INPUT_DATA" | grep -o '"message": *"[^"]*"' | cut -d'"' -f4)
STYLE=$(echo "$INPUT_DATA" | grep -o '"style": *"[^"]*"' | cut -d'"' -f4)
if [ -z "$AUTHOR" ]; then
echo "❌ Fehler: Konnte keinen Autor im JSON finden. Ist es das richtige Format?"
exit 1
fi
echo "✂️ Schnippsi begutachtet das Design..."
if [[ "$STYLE" == "glassmorphism" ]]; then
echo " \"Ohhh, Glassmorphism! Sehr modern. Durchscheinend und elegant. 10/10 Style-Punkte!\" ✨"
else
echo " \"Style: $STYLE. Interessant, aber ist es 'très chic'?\""
fi
echo ""
sleep 1
echo "🏛️ Templatus prüft die Struktur..."
LENGTH=${#MESSAGE}
echo " \"Die Nachricht ist $LENGTH Zeichen lang. Ein stabiler Block im DOM.\""
echo ""
sleep 1
echo "🌶️ PepperPHP liest den Inhalt..."
echo " \"Hallo $AUTHOR! Deine Nachricht wurde in den Baum geritzt:\""
echo ""
echo " 📝 \"$MESSAGE\""
echo ""
echo "🌳 Der Wald hat dein Zeichen angenommen."

38
release_notes.md Normal file
View File

@@ -0,0 +1,38 @@
# 🌲 Crumbforest v0.0-RC3: "Dein Zeichen im Wald"
**"Etwas bauen was noch keiner gebaut hat"** Die Crumbblocks Ära beginnt!
Dieser Release Candidate bringt das erste visuelle Design-Abenteuer in den Wald und verbindet die Browser-Welt mit dem Terminal.
## 🎨 Neue Features
### 1. Crumbblocks Integration
- **Browser-to-Terminal Bridge:** Baue Code oder UI im Browser, schicke die Daten per Clipboard an die Crew im Terminal.
- **Start-Helper:** `./start_crumbblocks.sh` startet automatisch lokalen Webserver (PHP/Python) und Browser.
### 2. Neue Mission: "Dein Zeichen im Wald"
- **Kategorie:** 🏆 Challenges
- **Ziel:** Erstelle ein digitales "Blatt" mit HTML/CSS im Glassmorphism-Stil.
- **Tech-Stack:** 100% Single-File HTML/CSS/JS. Keine externen deps.
- **Workflow:**
1. Intro mit Templatus & Schnippsi (`./missions/challenges/schnippsi_ui_design.sh`)
2. Design im Browser (`crumbblocks/schnippsi_ui.html`)
3. Auswertung & Feedback (`./evaluate_mission_data.sh`)
### 3. Smart Evaluation Gateway
- Das Skript `evaluate_mission_data.sh` ist jetzt intelligent!
- Es erkennt automatisch, ob es sich um Roboter-Daten oder ein UI-Design handelt.
- **Auto-Routing:** Leitet UI-JSON automatisch an Schnippsi (`evaluate_sign.sh`) weiter.
### 4. Logging & Bugfixes
- **Templatus & Schnippsi:** Pfad-Fehler (`/home/zero`) auf macOS behoben.
- **Logging:** Alle Logs landen jetzt zuverlässig in deinem Repo-Ordner (`logs/`), wo sie hingehören.
## 🤖 Crew Updates
- **Schnippsi:** Hat jetzt ein Auge für Ästhetik und Glassmorphism.
- **Templatus:** Baut stabile HTML5-Gerüste auch auf Mac.
- **PepperPHP:** Backend-Logik für JSON-Export verbessert.
---
**"Der Wald ist nie fertig - er wächst mit jeder Idee!"** 🌱

71
start_crumbblocks.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/bash
# 🌍 Startet den Crumbblocks Server
# Damit die Clipboard-API funktioniert, brauchen wir "localhost".
# Dieses Script startet einen mini Python-Server und öffnet den Browser.
# Waldwächter laden (für Pfade, optional)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${SCRIPT_DIR}" # Wir starten direkt im Root
# Port
PORT=8123
URL="http://localhost:${PORT}/crumbblocks/rainbow_counter.html"
clear
echo "🌍 CRUMBBLOCKS SERVER START"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Server-Auswahl (PHP bevorzugt via User-Request, sonst Python)
if command -v php &>/dev/null; then
SERVER_CMD="php -S localhost:$PORT"
echo "🐘 PHP gefunden! Starte PHP Development Server..."
elif command -v python3 &>/dev/null; then
SERVER_CMD="python3 -m http.server $PORT"
echo "🐍 Python 3 gefunden! Starte http.server..."
elif command -v python &>/dev/null; then
SERVER_CMD="python -m http.server $PORT"
echo "🐍 Python gefunden! Starte http.server..."
else
echo "❌ Weder PHP noch Python gefunden."
echo "Bitte installiere eines von beiden für den lokalen Server."
exit 1
fi
echo "🚀 Starte Server auf Port $PORT..."
echo "👉 $URL"
echo ""
echo "Drücke [CTRL+C] um den Server zu stoppen."
echo ""
# Server im Hintergrund starten
cd "$ROOT_DIR"
# Wir führen den Befehl aus der Variable aus (shell splitting allowed here)
$SERVER_CMD >/dev/null 2>&1 &
SERVER_PID=$!
# Warten bis Server da ist (kurz)
sleep 1
# Browser öffnen
if [[ "$OSTYPE" == "darwin"* ]]; then
open "$URL"
elif command -v xdg-open &>/dev/null; then
xdg-open "$URL"
else
echo "⚠️ Konnte Browser nicht automatisch öffnen."
echo "Bitte öffne: $URL"
fi
# Auf CTRL+C warten (Trap für Cleanup)
cleanup() {
echo ""
echo "🛑 Stoppe Server (PID $SERVER_PID)..."
kill "$SERVER_PID" 2>/dev/null
echo "✅ Tschüss!"
exit
}
trap cleanup SIGINT
# Endlosschleife damit Script offen bleibt
wait $SERVER_PID