feat(rc3): Crumbblocks UI Mission & Smart Routing 🎨✨
This commit is contained in:
14
README.md
14
README.md
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
628
crumbblocks/bezier.html
Normal 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>One‑Liner 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>One‑Liner 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>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‑Pen (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>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="hint">Klicke einen Stroke → <em>Bearbeiten</em>. Doppelklick auf Pfad fügt einen Anker. <span class="kbd">L</span> toggelt Spiegel‑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>
|
||||
</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 Bezier‑Pen 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('Motion‑JSON 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>
|
||||
452
crumbblocks/bezier_stream.html
Normal file
452
crumbblocks/bezier_stream.html
Normal 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>
|
||||
366
crumbblocks/crumbblocks.html
Normal file
366
crumbblocks/crumbblocks.html
Normal 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>. 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
298
crumbblocks/index.html
Normal 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
278
crumbblocks/index_v1.html
Normal 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>
|
||||
575
crumbblocks/lipo_6s_charger.html
Normal file
575
crumbblocks/lipo_6s_charger.html
Normal 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>
|
||||
453
crumbblocks/lipo_6s_charger_sim_safe_v2.html
Normal file
453
crumbblocks/lipo_6s_charger_sim_safe_v2.html
Normal 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>
|
||||
322
crumbblocks/lipo_6s_charger_sim_safe_v4.html
Normal file
322
crumbblocks/lipo_6s_charger_sim_safe_v4.html
Normal 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>
|
||||
322
crumbblocks/lipo_6s_charger_sim_safe_v5.html
Normal file
322
crumbblocks/lipo_6s_charger_sim_safe_v5.html
Normal 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>
|
||||
409
crumbblocks/lipo_6s_charger_sim_safe_v6.html
Normal file
409
crumbblocks/lipo_6s_charger_sim_safe_v6.html
Normal 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>
|
||||
500
crumbblocks/lipo_6s_charger_sim_safe_v7.html
Normal file
500
crumbblocks/lipo_6s_charger_sim_safe_v7.html
Normal 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>
|
||||
|
||||
22
crumbblocks/mock_ws_server.js
Normal file
22
crumbblocks/mock_ws_server.js
Normal 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
395
crumbblocks/painter.html
Normal 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>One‑Liner Painter (Multi‑Stroke) – 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>One‑Liner Painter (Multi‑Stroke) → 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/Stop‑Marker</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>Stroke‑Farbe A</span></label>
|
||||
<label class="chip"><input type="radio" name="mode" value="B"> <span>Stroke‑Farbe 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 (JSON‑only)</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="Strich‑Liste"></ul>
|
||||
<p class="muted">Neuer Stroke beginnt mit <span class="kbd">Maus/Touch‑Down</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">Motion‑JSON 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 – One‑Liner"></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 – One‑Liner</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 Motion‑JSON.</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() || 'One‑Liner';
|
||||
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('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); });
|
||||
|
||||
// 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>
|
||||
647
crumbblocks/painter_lines.html
Normal file
647
crumbblocks/painter_lines.html
Normal 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>
|
||||
395
crumbblocks/painter_spray.html
Normal file
395
crumbblocks/painter_spray.html
Normal 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>One‑Liner Painter (Multi‑Stroke) – 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>One‑Liner Painter (Multi‑Stroke) → 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/Stop‑Marker</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>Stroke‑Farbe A</span></label>
|
||||
<label class="chip"><input type="radio" name="mode" value="B"> <span>Stroke‑Farbe 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 (JSON‑only)</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="Strich‑Liste"></ul>
|
||||
<p class="muted">Neuer Stroke beginnt mit <span class="kbd">Maus/Touch‑Down</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">Motion‑JSON 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 – One‑Liner"></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 – One‑Liner</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 Motion‑JSON.</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() || 'One‑Liner';
|
||||
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('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); });
|
||||
|
||||
// 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>
|
||||
722
crumbblocks/rainbow_counter.html
Normal file
722
crumbblocks/rainbow_counter.html
Normal 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“/„T1–T3“ 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
31
crumbblocks/readme.txt
Normal file
@@ -0,0 +1,31 @@
|
||||
# Schnippsi Painter — Streaming Quickstart
|
||||
|
||||
## 1) Datei
|
||||
Öffne `schnippsi_painter_stream.html` im Browser. Zeichne frei oder mit dem Bezier‑Tool.
|
||||
- `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 Mini‑WS:
|
||||
```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, ARIA‑Status, hoher Kontrast, 200% Zoom stabil.
|
||||
293
crumbblocks/schnippsi_ui.html
Executable file
293
crumbblocks/schnippsi_ui.html
Executable 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>
|
||||
@@ -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"
|
||||
|
||||
8
missions/challenges/schnippsi_ui_design.meta.json
Normal file
8
missions/challenges/schnippsi_ui_design.meta.json
Normal 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
|
||||
}
|
||||
70
missions/challenges/schnippsi_ui_design.sh
Executable file
70
missions/challenges/schnippsi_ui_design.sh
Executable 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
133
missions/evaluate_mission_data.sh
Executable 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
60
missions/evaluate_sign.sh
Executable 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
38
release_notes.md
Normal 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
71
start_crumbblocks.sh
Executable 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
|
||||
Reference in New Issue
Block a user