14 Commits

Author SHA1 Message Date
Branko May Trinkwald
d4d75af428 🐼 feat(dojo): BashPanda Gürtel-System - Von Schwarz auf Weiss 🥋
Der 18. Waldwächter betritt den Wald: BashPanda lehrt Bash als Kampfkunst!

 Neue Features:

🐼 BashPanda Waldwächter:
- Kung Fu Meister Persönlichkeit
- Lehrt Bash durch Kampfkunst-Metaphern
- Integriert in waldwaechter.sh Library
- Crew Memory: Kennt alle anderen Waldwächter

🥋 6 Gürtel-Missionen (Progressive Bash-Meisterschaft):
- 🖤 Schwarzer Gürtel: echo, Variablen, read, ANSI codes
- 💖 Pinker Gürtel: if/then, while/for, Arrays, Arithmetik
- 💙 Blauer Gürtel: sed, case, bc, Textverarbeitung
- 💚 Grüner Gürtel: grep, regex, Pattern Matching
- 💛 Gelber Gürtel: Funktionen, source, Parameter
- 🤍 Weisser Gürtel: Background jobs, Prozesse, Parallelität

📝 Interaktives Quiz-System:
- Browser-based Gürtelprüfung (crumbblocks)
- 30 Fragen (5 pro Gürtel)
- Farbcodiert nach Gürtel
- Auto-Export via Clipboard
- Terminal-Auswertung mit Zertifikaten

🎓 Zertifikate-System:
- Automatische Generierung bei 80%+
- Gespeichert in logs/zertifikate/
- BashPanda's Segen & Weisheiten

📚 Dokumentation:
- CLAUDE.md komplett aktualisiert
- BashPanda als 18. Waldwächter dokumentiert
- Vollständige Dojo-Architektur beschrieben

"Der Weg des Codes ist wie der Weg der Kampfkunst:
 Geduld, Präzision, Wiederholung."

 - BashPanda 🐼🎋

Crumbforest wächst! 🌲 Der Wurzelbau geht weiter! 🌳

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-24 00:29:30 +01:00
Branko May Trinkwald
554b04ee17 schnippsi ui editor v.0.0 2025-12-23 23:11:00 +01:00
Branko May Trinkwald
08dd5605a8 fun in the sun <3 2025-12-23 22:25:11 +01:00
Branko May Trinkwald
01a01f53b4 feat(rc3): Crumbblocks UI Mission & Smart Routing 🎨 2025-12-23 20:52:00 +01:00
Branko May Trinkwald
e52684d6f8 🐛 Fix: eule_tokens - Robuste Token-Log-Parsing
Problem:
- eule_tokens produzierte jq-Fehler:
  "Cannot index array with string 'zeit'"
  "Cannot index array with string 'usage'"
- Gleiche Ursache wie bei check_token_budget
- Token-Logs haben [] als erste Zeile
- usage kann String oder Objekt sein

Lösung:
- Skip [] und leere Zeilen mit continue
- jq mit Fallback: '.zeit // "unknown"'
- Unterstütze beide usage-Formate:
  * String: usage | fromjson | .total_tokens
  * Objekt: usage.total_tokens
- Validiere Zahlen vor Addition

Test:
 Keine jq-Fehler mehr
 Token werden korrekt summiert
 Funktioniert mit allen Log-Formaten

Jetzt können Kinder ihre Token-Nutzung ohne Fehler sehen! 🌲

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-22 13:42:29 +01:00
Branko May Trinkwald
223fc5f37d 🔧 Fix: Robustere Token-Berechnung für Budget-Check
Problem:
- check_token_budget() hatte Probleme mit inkonsistenten Log-Formaten
- Manche Logs haben usage als String (escaped JSON)
- Andere haben usage als Objekt
- grep-Pattern war zu strikt (\\"$today\\" fand nichts)

Lösung:
- Grep ohne escaped quotes (grep "$today" statt grep \"\\"$today\\"\")
- jq-Abfrage unterstützt BEIDE Formate:
  * usage als String → fromjson → total_tokens
  * usage als Objekt → .total_tokens
- Validierung: Nur valide Zahlen werden addiert
- awk summiert alle Tokens pro Log-Datei

Test-Ergebnis:
 Budget = 0 (unbegrenzt) → Erlaubt
 Budget = 100, Verbrauch = 150 → Blockiert mit Nachricht
 Budget = 1000, Verbrauch = 50 → Erlaubt

Kinderfreundliche Blockierungs-Nachricht:
"Liebes Kind, heute hast du schon X Tokens verwendet.
 💚 Jede Frage ist wertvoll - aber auch Pausen sind wichtig."

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-22 00:06:44 +01:00
Branko May Trinkwald
4005bb9b18 🔒 Security: Token-Budget-Enforcement für Kinderschutz
KRITISCHES SICHERHEITSUPDATE für alle 17 Waldwächter-Scripts.

Problem behoben:
- Token-Budget wurde nur angezeigt, aber NICHT durchgesetzt
- Kinder konnten unbegrenzt API-Calls machen → Kostenrisiko

Implementierung:
1. check_token_budget() Funktion in lib/waldwaechter.sh
   - Berechnet täglichen Token-Verbrauch
   - Vergleicht mit DAILY_TOKEN_BUDGET aus .env
   - Budget = 0 oder leer → unbegrenzt
   - Budget überschritten → freundliche Blockierung

2. Budget-Check in ALLEN 17 Waldwächter-Scripts:
   - Prüfung VOR jedem API-Call
   - Kinderfreundliche Nachricht bei Limit
   - Warnung bei knappem Budget

Philosophie: "Was kostet die Frage eines Kindes?"
→ Im Wald unbezahlbar, im System achtsam begrenzt.

Scripts aktualisiert:
 mayaeule, deepbit, bugsy, schnippsi, templatus, tobi
 schraubaer, schnecki, dumbosql, funkfox, taichitaube
 snakepy, pepperphp, crabbyrust, spider, vektor, asciimonster

Test-Ergebnisse:
- Syntax-Check: 17/17 bestanden
- Funktionstest: Budget-Enforcement funktioniert
- Unbegrenzt-Modus: funktioniert
- Limit-Modus: blockiert korrekt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-22 00:01:15 +01:00
Branko May Trinkwald
55307f4a76 🐛 Fix: crew_memory - Suche in Waldwächter-Logs jetzt funktional
Zwei kritische Bugs behoben:

1. Kaputtes Glob-Pattern in Zeile 212
   - Vorher: verwendete ${history_file} bevor Variable definiert war
   - Nachher: sauberes /*/*_history.json Pattern

2. Falsche JSON-Verarbeitung in Zeile 217
   - Vorher: grep auf Rohtext → jq Parse-Fehler
   - Nachher: jq durchsucht direkt die JSON-Struktur (case-insensitive)

Ergebnis: crew_memory findet jetzt alle Treffer in den Logs! 🌲

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-21 23:38:43 +01:00
Branko May Trinkwald
2ff177d0e8 🎉 RC0 - Release Candidate 0
Der Crumbforest ist bereit.

Heute entstanden:
• 17 Waldwächter - Alle Egos vereint und funktional
• Input-Transparenz - crew_tokens zeigt 21,471 Tokens
• Output-Transparenz - crew_memo trackt kreative Krümel
• Kollaborative Wurzeln - seeds/ für gemeinsames Wachsen
• Pfad-Fixes - CREW_DIR, waldwaechter.sh Integration

Architektur:
- 1 Script (waldwaechter.sh)
- 1 Ordner voller Weisheit (crumbforest_roles/)
- 17 Egos mit Persönlichkeit
- 0 Regeln

Philosophie:
eule -> kleine krümel -> tools -> nullfeld

Der Wald mag keine Sticker.
17 Egos im Wald. 1 Wald ohne Ego. Zero Rules.

🌲 Gewachsen im Nullfeld
   #ozmai #mensch #maschine
2025-12-21 23:21:44 +01:00
Branko May Trinkwald
6fec2df7d9 🌱 Seed: Wurzel-Dateien für kollaborative Erweiterung
Neues seeds/ Verzeichnis mit:
- crumb_memo.seed.json - Erste Krümel als Ausgangspunkt
- README.md - Philosophie & Verwendung

Die ersten drei Krümel im Nullfeld:
  🎧 Digi's Mixcloud - digfafunk
  💾 CF_Zero_V1 Repo - crumbmissions
  🔊 Digi's SoundCloud - digfafunk

Philosophie:
"Ein Baum braucht eine Wurzel. Ein Wald braucht den ersten Baum."

Diese Seeds sind Ausgangspunkte - erweiterbar von allen.
CrewLove: Gemeinsam wachsen im Nullfeld 💚🌲

Transparenz in beide Richtungen:
- Input: crew_tokens (Was frage ich?)
- Output: crew_memo (Was schaffe ich?)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-21 23:09:21 +01:00
Branko May Trinkwald
599af5b011 New: Crumb Memo - Output-Transparenz System
Neue Befehle für kreatives Output-Tracking:
- crumb_memo <link> [notiz] - Kreativen Link festhalten
- crew_memo - Alle kreativen Krümel anzeigen

Features:
• Auto-Erkennung von Plattformen:
  🎧 Mixcloud, 🔊 SoundCloud, 📹 YouTube, 💾 Git
• Zeitstempel & Notizen
• JSON-basiert in logs/crumb_memo.json
• Statistik nach Typ

Philosophie:
Input-Transparenz (crew_tokens) + Output-Transparenz (crew_memo)
= Vollständige Sichtbarkeit der Krümel im Nullfeld

"Was habe ich geschaffen?" 💚

Beispiel:
  crumb_memo "https://mixcloud.com/digfafunk/" "New Mix"
  crew_memo

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-21 23:01:47 +01:00
Branko May Trinkwald
0f8946e4c4 🔍 Fix: crew_tokens zeigt jetzt alle Token transparent
Problem: crew_tokens suchte in alten ~/.xxx_logs Pfaden,
aber Waldwächter loggen jetzt in repo/logs/.

Lösung:
- waldwaechter.sh im CrumbCrew RC-File sourcen
- Alte crew_tokens/crew_memory Funktionen entfernt
- Nutzt jetzt bessere Versionen aus waldwaechter.sh

Resultat: Transparenz! 21,471 Tokens sichtbar 💚
"Was kostet die Frage eines Kindes?" - Krümel im Nullfeld

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-21 21:49:36 +01:00
Branko May Trinkwald
3ca1709eb3 Add: Templatus - 17. Waldwächter komplett
Templatus (📄 Template-Master) war im waldwaechter.sh
vorhanden, fehlte aber im Mission Selector.

Hinzugefügt:
- templatus() Funktion im CrumbCrew Command Central
- Eintrag in crew_status CHARS array
- Dokumentation in crew_help
- Beispiel: "Erstelle eine HTML5 Struktur"

Jetzt alle 17 Waldwächter wie nach OZMAI vorgesehen! 🌲

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-21 21:35:51 +01:00
Branko May Trinkwald
e1cae4fbd1 🐛 Fix: CREW_DIR path nach waldwaechter.sh source
waldwaechter.sh überschrieb SCRIPT_DIR, was zu falschem
CREW_DIR=/lib/crumbforest_roles führte. Alle Waldwächter
zeigten "Nicht verfügbar" im crew_status.

Fix: REPO_ROOT vor source speichern und für Pfade nutzen.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-21 21:29:19 +01:00
73 changed files with 15867 additions and 6308 deletions

1133
CLAUDE.md

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,13 @@ Drei komplette Missionen zum Roboter-Bauen mit der ganzen Crew:
- OpenCV + Weather APIs + Astronomical calculations
- Bonus: Learn about Lunar Rainbows! 🌙🌈
### ☀️ Solar Wasserkocher
"Physik ist der Algorithmus der Natur"
- 6 Phasen: Energie → Schleifen → Optimierung
- Simuliere einen solargetriebenen Wasserkocher mit Physik-Engine
- 3 Waldwächter: CapaciTobi, Schnecki, FunkFox
- Lerne: Variablen, While-Loops und Energie-Management ($E = c \cdot m \cdot \Delta T$)
## 📚 Features
- **Interaktive Lernmissionen** - Von Basics bis Advanced & Robots
@@ -101,6 +108,7 @@ Drei komplette Missionen zum Roboter-Bauen mit der ganzen Crew:
- **🔋 LiPo Power Academy** - Batterie-Wissen für Roboter
- **🌈 Regenbogen-Zählmaschine** - Farb-Event-Counter
- **🌙 Mond Maschine** - Rainbow Predictor mit Computer Vision
- **☀️ Solar Wasserkocher** - Physik-Simulation & Schleifen-Logik
## 🤖 Waldwächter nutzen
@@ -253,14 +261,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)
---

20
VERSION Normal file
View File

@@ -0,0 +1,20 @@
RC0 - Release Candidate 0
Datum: 2025-12-21
🌲 Der Crumbforest ist bereit
Was entstanden ist:
• 17 Waldwächter - Alle Egos vereint
• Input-Transparenz - crew_tokens (Token-Tracking)
• Output-Transparenz - crew_memo (Krümel-Tracking)
• Kollaborative Wurzeln - seeds/ für gemeinsames Wachsen
• Zero Rules - Ein Script, ein Ordner, unendlich Möglichkeiten
Philosophie:
eule -> kleine krümel -> tools -> nullfeld
#ozmai #mensch #maschine
Der Wald mag keine Sticker.
17 Egos im Wald. 1 Wald ohne Ego. 0 Regeln.
🦉 Entstanden im Nullfeld 💚

View File

@@ -15,6 +15,7 @@ NC='\033[0m' # No Color
# === KONFIGURATION ===
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="${SCRIPT_DIR}" # Save repo root before sourcing waldwaechter.sh
MISSION_DIR="${SCRIPT_DIR}/missions"
ENV_FILE="${SCRIPT_DIR}/.env"
@@ -499,7 +500,7 @@ function mayaeule_doktor() {
EULE_RC="/tmp/crumb_eule_$$.rc"
# Absoluter Pfad zum Maya-Eule Script
MAYAEULE_PATH="${SCRIPT_DIR}/crumbforest_roles/mayaeule_zero.sh"
MAYAEULE_PATH="${REPO_ROOT}/crumbforest_roles/mayaeule_zero.sh"
cat > "${EULE_RC}" << EOF
# Load .bashrc if exists
@@ -564,22 +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
while IFS= read -r line; do
zeit=\$(echo "\$line" | jq -r '.zeit')
tokens=\$(echo "\$line" | jq -r '.usage.total_tokens')
TOTAL=\$((TOTAL + tokens))
echo " \$zeit: \$tokens Tokens"
done < "\$LOGDIR/token_log.json"
if [[ -d "\$LOGDIR" ]]; then
local total_tokens=0
local found_any=false
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
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
}
@@ -616,7 +658,7 @@ function crumbcrew_doktor() {
CREW_RC="/tmp/crumb_crew_$$.rc"
# Pfade zu allen Charakteren
CREW_DIR="${SCRIPT_DIR}/crumbforest_roles"
CREW_DIR="${REPO_ROOT}/crumbforest_roles"
cat > "${CREW_RC}" << EOF
# Load .bashrc if exists
@@ -635,9 +677,15 @@ NC='\033[0m'
# Prompt im CrumbCrew-Style
export PS1="\[\033[1;32m\](🌲 CrumbCrew) \u@\h:\w\$ \[\033[0m\]"
# Pfad zu den Charakteren
# Repo Root und Pfade
REPO_ROOT="${REPO_ROOT}"
CREW_DIR="${CREW_DIR}"
# Lade Waldwächter Library für crew_tokens, crew_status, etc.
if [[ -f "\${REPO_ROOT}/lib/waldwaechter.sh" ]]; then
source "\${REPO_ROOT}/lib/waldwaechter.sh"
fi
# === WALDWÄCHTER FUNKTIONEN ===
function mayaeule() {
@@ -676,6 +724,15 @@ function schnippsi() {
"\$CREW_DIR/schnippsi_zero.sh" "\$@"
}
function templatus() {
if [[ -z "\$1" ]]; then
echo -e "\${YELLOW}Verwendung: templatus \"Deine Frage\"\${NC}"
return
fi
echo -e "\${BLUE}📄 Templatus strukturiert...\${NC}"
"\$CREW_DIR/templatus_zero.sh" "\$@"
}
function tobi() {
if [[ -z "\$1" ]]; then
echo -e "\${YELLOW}Verwendung: tobi \"Deine Frage\"\${NC}"
@@ -801,6 +858,7 @@ function crew_help() {
echo -e " \${BLUE}🐙 deepbit\${NC} - Der poetische Oktopus (Bash-Konzepte)"
echo -e " \${RED}🐞 bugsy\${NC} - Der Debugging-Clown (Fehlersuche)"
echo -e " \${CYAN}✂️ schnippsi\${NC} - Der Shell-Helfer (Kommandos)"
echo -e " \${BLUE}📄 templatus\${NC} - Der Template-Master (HTML, Struktur)"
echo -e " \${GREEN}🤖 tobi\${NC} - Der Daten-Experte (JSON, Daten)"
echo -e " \${CYAN}🔧 schraubaer\${NC} - Der Handwerker (Werkzeug, Schweißen)"
echo -e " \${GREEN}🐌 schnecki\${NC} - Der Elektronik-Bastler (Löten, Sensoren)"
@@ -830,6 +888,7 @@ function crew_help() {
echo " deepbit \"Erkläre Pipes und Redirects\""
echo " bugsy \"Warum funktioniert mein Script nicht?\""
echo " schnippsi \"Wie nutze ich grep?\""
echo " templatus \"Erstelle eine HTML5 Struktur\""
echo " tobi \"Parse dieses JSON\""
echo " schraubaer \"Welches Werkzeug brauche ich zum Löten?\""
echo " schnecki \"Wie löte ich eine LED an einen Widerstand?\""
@@ -854,7 +913,7 @@ function crew_status() {
echo -e "\${CYAN}🌲 CrumbCrew Status\${NC}"
echo ""
CHARS=("mayaeule:🦉:Maya-Eule" "deepbit:🐙:Deepbit" "bugsy:🐞:Bugsy" "schnippsi:✂️:Schnippsi" "tobi:🤖:Tobi" "schraubaer:🔧:Schraubbär" "schnecki:🐌:Schnecki" "dumbosql:🐘:DumboSQL" "funkfox:🦊:FunkFox" "taichitaube:🕊️:TaichiTaube" "snakepy:🐍:SnakePy" "pepperphp:🧓:PepperPHP" "crabbyrust:🦀:CrabbyRust" "spider:🕷️:Spider" "vektor:🧭:Vektor" "asciimonster:👾:ASCII-Monster")
CHARS=("mayaeule:🦉:Maya-Eule" "deepbit:🐙:Deepbit" "bugsy:🐞:Bugsy" "schnippsi:✂️:Schnippsi" "templatus:📄:Templatus" "tobi:🤖:Tobi" "schraubaer:🔧:Schraubbär" "schnecki:🐌:Schnecki" "dumbosql:🐘:DumboSQL" "funkfox:🦊:FunkFox" "taichitaube:🕊️:TaichiTaube" "snakepy:🐍:SnakePy" "pepperphp:🧓:PepperPHP" "crabbyrust:🦀:CrabbyRust" "spider:🕷️:Spider" "vektor:🧭:Vektor" "asciimonster:👾:ASCII-Monster")
for char_info in "\${CHARS[@]}"; do
IFS=: read -r name icon display <<< "\$char_info"
@@ -883,52 +942,8 @@ function crew_status() {
fi
}
function crew_tokens() {
echo -e "\${CYAN}📊 CrumbCrew Token-Verbrauch\${NC}"
echo ""
TOTAL=0
for logdir in ~/.{mayaeule,eule,deepbit,bugsy,schnippsi,tobi,schraubaer,schnecki,dumbosql,funkfox,taichitaube,snakepy,pepperphp,crabbyrust,spider,vektor,asciimonster}_logs; do
if [[ -d "\$logdir" ]] && [[ -f "\$logdir/token_log.json" ]]; then
char_name=\$(basename "\$logdir" | sed 's/_logs//')
char_tokens=0
while IFS= read -r line; do
tokens=\$(echo "\$line" | jq -r '.usage.total_tokens' 2>/dev/null)
if [[ "\$tokens" != "null" ]] && [[ -n "\$tokens" ]]; then
char_tokens=\$((char_tokens + tokens))
fi
done < "\$logdir/token_log.json"
if [[ \$char_tokens -gt 0 ]]; then
echo -e " \${GREEN}\$char_name:\${NC} \$char_tokens Tokens"
TOTAL=\$((TOTAL + char_tokens))
fi
fi
done
echo ""
echo -e "\${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\${NC}"
echo -e " \${GREEN}Gesamt: \$TOTAL Tokens\${NC}"
echo -e " \${CYAN}Jede Frage ist wertvoll 🌲\${NC}"
echo -e "\${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\${NC}"
}
function crew_memory() {
echo -e "\${CYAN}📜 CrumbCrew Erinnerungen\${NC}"
echo ""
for logdir in ~/.{mayaeule,eule,deepbit,bugsy,schnippsi,tobi,schraubaer,schnecki,dumbosql,funkfox,taichitaube,snakepy,pepperphp,crabbyrust,spider,vektor,asciimonster}_logs; do
if [[ -d "\$logdir" ]] && [[ -f "\$logdir/*_history.json" ]]; then
char_name=\$(basename "\$logdir" | sed 's/_logs//')
count=\$(jq '. | length' "\$logdir/*_history.json" 2>/dev/null)
if [[ "\$count" != "null" ]] && [[ \$count -gt 0 ]]; then
echo -e " \${GREEN}\$char_name:\${NC} \$count Gespräche"
fi
fi
done
}
# crew_tokens, crew_memory, crew_status, crew_doctor, crew_syntax, crew_help
# sind bereits in waldwaechter.sh definiert und werden automatisch geladen
alias help="crew_help"
alias status="crew_status"

View File

@@ -0,0 +1,631 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🐼 BashPanda Gürtelprüfung 🥋</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Courier New', monospace;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
color: #e0e0e0;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.header {
text-align: center;
padding: 30px 20px;
background: rgba(0,0,0,0.3);
border-radius: 15px;
margin-bottom: 30px;
border: 2px solid #4a4a4a;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
.header .subtitle {
font-size: 1.2em;
opacity: 0.8;
}
.belt-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.belt-btn {
padding: 20px;
border: 3px solid;
border-radius: 10px;
cursor: pointer;
text-align: center;
font-size: 1.1em;
font-weight: bold;
transition: all 0.3s;
background: rgba(0,0,0,0.3);
}
.belt-btn:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
}
.belt-btn.active {
box-shadow: 0 0 20px currentColor;
background: rgba(255,255,255,0.1);
}
.black { border-color: #000; color: #fff; }
.pink { border-color: #ff69b4; color: #ff69b4; }
.blue { border-color: #4169e1; color: #4169e1; }
.green { border-color: #32cd32; color: #32cd32; }
.yellow { border-color: #ffd700; color: #ffd700; }
.white { border-color: #f0f0f0; color: #f0f0f0; }
.quiz-container {
background: rgba(0,0,0,0.3);
border-radius: 15px;
padding: 30px;
border: 2px solid #4a4a4a;
display: none;
}
.quiz-container.active {
display: block;
}
.question {
margin-bottom: 30px;
}
.question-text {
font-size: 1.3em;
margin-bottom: 20px;
padding: 15px;
background: rgba(255,255,255,0.05);
border-left: 4px solid;
border-radius: 5px;
}
.options {
display: flex;
flex-direction: column;
gap: 10px;
}
.option {
padding: 15px 20px;
border: 2px solid #4a4a4a;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background: rgba(0,0,0,0.2);
}
.option:hover {
background: rgba(255,255,255,0.1);
border-color: #6a6a6a;
}
.option.selected {
border-color: #ffd700;
background: rgba(255, 215, 0, 0.2);
}
.option.correct {
border-color: #32cd32;
background: rgba(50, 205, 50, 0.2);
}
.option.wrong {
border-color: #ff4444;
background: rgba(255, 68, 68, 0.2);
}
.controls {
display: flex;
gap: 15px;
margin-top: 30px;
}
.btn {
flex: 1;
padding: 15px 30px;
border: 2px solid #4a4a4a;
border-radius: 8px;
background: rgba(0,0,0,0.3);
color: #e0e0e0;
font-size: 1.1em;
cursor: pointer;
transition: all 0.3s;
}
.btn:hover {
background: rgba(255,255,255,0.1);
border-color: #6a6a6a;
}
.btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #764ba2;
}
.btn-primary:hover {
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.results {
display: none;
text-align: center;
padding: 40px;
background: rgba(0,0,0,0.3);
border-radius: 15px;
border: 2px solid #4a4a4a;
}
.results.show {
display: block;
}
.results h2 {
font-size: 2.5em;
margin-bottom: 20px;
}
.results .score {
font-size: 3em;
font-weight: bold;
margin: 30px 0;
}
.results .verdict {
font-size: 1.5em;
margin: 20px 0;
padding: 20px;
border-radius: 10px;
background: rgba(255,255,255,0.05);
}
.export-info {
margin-top: 30px;
padding: 20px;
background: rgba(255, 215, 0, 0.1);
border: 2px solid #ffd700;
border-radius: 10px;
}
.progress {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
padding: 10px;
background: rgba(0,0,0,0.2);
border-radius: 8px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.pulse {
animation: pulse 2s infinite;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🐼 BashPanda Gürtelprüfung 🥋</h1>
<div class="subtitle">"Beweise dein Wissen, junger Schüler"</div>
</div>
<div class="belt-selector" id="beltSelector">
<div class="belt-btn black" data-belt="schwarz">🖤<br>Schwarz</div>
<div class="belt-btn pink" data-belt="pink">💖<br>Pink</div>
<div class="belt-btn blue" data-belt="blau">💙<br>Blau</div>
<div class="belt-btn green" data-belt="gruen">💚<br>Grün</div>
<div class="belt-btn yellow" data-belt="gelb">💛<br>Gelb</div>
<div class="belt-btn white" data-belt="weiss">🤍<br>Weiss</div>
</div>
<div class="quiz-container" id="quizContainer">
<div class="progress" id="progress"></div>
<div id="questionContainer"></div>
<div class="controls">
<button class="btn" id="prevBtn" disabled>⬅️ Zurück</button>
<button class="btn btn-primary" id="nextBtn">Weiter ➡️</button>
</div>
</div>
<div class="results" id="results"></div>
</div>
<script>
const quizData = {
schwarz: [
{
question: "Was gibt 'echo $HOME' aus?",
options: ["Dein Home-Verzeichnis", "Die Umgebungsvariable HOME", "Beide Antworten sind richtig", "Einen Fehler"],
correct: 2,
color: "#fff"
},
{
question: "Wie erstellt man eine Variable?",
options: ["name = value", "name=value", "$name=value", "set name=value"],
correct: 1,
color: "#fff"
},
{
question: "Was macht 'read -p \"Name: \" name'?",
options: ["Liest eine Datei", "Zeigt 'Name:' und speichert Eingabe in $name", "Gibt $name aus", "Nichts"],
correct: 1,
color: "#fff"
},
{
question: "Welcher ANSI Code macht Text fett?",
options: ["\\e[0m", "\\e[1m", "\\e[4m", "\\e[32m"],
correct: 1,
color: "#fff"
},
{
question: "Was ist der Unterschied zwischen ' und \" in echo?",
options: ["Keiner", "' ignoriert Variablen, \" ersetzt sie", "\" ignoriert Variablen", "Beide ersetzen Variablen"],
correct: 1,
color: "#fff"
}
],
pink: [
{
question: "Was prüft [ $a -eq $b ]?",
options: ["String-Gleichheit", "Zahlen-Gleichheit", "Größer als", "Kleiner als"],
correct: 1,
color: "#ff69b4"
},
{
question: "Wie greift man auf das 3. Array-Element zu?",
options: ["${arr[3]}", "${arr[2]}", "$arr[2]", "arr[3]"],
correct: 1,
color: "#ff69b4"
},
{
question: "Was macht 'while [ $i -le 5 ]; do ... done'?",
options: ["Läuft 4 mal", "Läuft 5 mal", "Läuft 6 mal", "Endlosschleife"],
correct: 1,
color: "#ff69b4"
},
{
question: "Wie zählt man Arrays hoch?",
options: ["i++", "i=$((i + 1))", "let i++", "Alle sind richtig"],
correct: 1,
color: "#ff69b4"
},
{
question: "Was gibt ${#arr[@]} zurück?",
options: ["Erstes Element", "Letztes Element", "Anzahl Elemente", "Fehler"],
correct: 2,
color: "#ff69b4"
}
],
blau: [
{
question: "Was macht 'sed s/alt/neu/'?",
options: ["Löscht 'alt'", "Ersetzt erstes 'alt' mit 'neu'", "Ersetzt alle 'alt' mit 'neu'", "Sucht nach 'alt'"],
correct: 1,
color: "#4169e1"
},
{
question: "Wofür nutzt man 'case' statt if/then?",
options: ["Für Schleifen", "Für mehrere if-Alternativen", "Für Arrays", "Für Funktionen"],
correct: 1,
color: "#4169e1"
},
{
question: "Wie rechnet man mit Fließkommazahlen?",
options: ["$(( ))", "bc", "let", "expr"],
correct: 1,
color: "#4169e1"
},
{
question: "Was bedeutet das * in case?",
options: ["Multiplikation", "Wildcard (alles)", "Default-Fall", "Fehler"],
correct: 2,
color: "#4169e1"
},
{
question: "Wie nutzt man bc für Division?",
options: ["bc 10/3", "echo '10/3' | bc", "10/3 | bc", "bc(10/3)"],
correct: 1,
color: "#4169e1"
}
],
gruen: [
{
question: "Was macht grep -i?",
options: ["Zeigt Zeilennummern", "Ignoriert Groß-/Kleinschreibung", "Invertiert Suche", "Rekursiv suchen"],
correct: 1,
color: "#32cd32"
},
{
question: "Was bedeutet ^ in Regex?",
options: ["Zeilenanfang", "Zeilenende", "Beliebiges Zeichen", "Wiederholung"],
correct: 0,
color: "#32cd32"
},
{
question: "Was matched [0-9]+ in Regex?",
options: ["Genau eine Ziffer", "Eine oder mehr Ziffern", "Null oder mehr Ziffern", "Keine Ziffer"],
correct: 1,
color: "#32cd32"
},
{
question: "Wie sucht man rekursiv in allen Dateien?",
options: ["grep muster *", "grep -r muster .", "grep -i muster", "grep -n muster"],
correct: 1,
color: "#32cd32"
},
{
question: "Was matched .* in Regex?",
options: ["Nichts", "Punkt und Stern", "Beliebige Zeichen (0+)", "Ein Zeichen"],
correct: 2,
color: "#32cd32"
}
],
gelb: [
{
question: "Wie definiert man eine Funktion?",
options: ["func name() {}", "function name() {}", "def name() {}", "Beide 1 und 2"],
correct: 3,
color: "#ffd700"
},
{
question: "Was ist $1 in einer Funktion?",
options: ["Erster Buchstabe", "Erster Parameter", "Exit-Code", "PID"],
correct: 1,
color: "#ffd700"
},
{
question: "Was macht 'source datei.sh'?",
options: ["Führt Datei aus", "Lädt Funktionen in aktuelle Shell", "Kopiert Datei", "Löscht Datei"],
correct: 1,
color: "#ffd700"
},
{
question: "Was macht 'return 42' in einer Funktion?",
options: ["Gibt 42 aus", "Beendet Skript", "Verlässt Funktion mit Code 42", "Fehler"],
correct: 2,
color: "#ffd700"
},
{
question: "Was ist $@ in einer Funktion?",
options: ["Erster Parameter", "Alle Parameter", "Anzahl Parameter", "Funktionsname"],
correct: 1,
color: "#ffd700"
}
],
weiss: [
{
question: "Was macht & am Ende eines Befehls?",
options: ["Beendet ihn", "Startet im Hintergrund", "Wiederholt ihn", "Nichts"],
correct: 1,
color: "#f0f0f0"
},
{
question: "Was ist $! ?",
options: ["Exit-Code", "PID des letzten Background-Jobs", "Anzahl Parameter", "Home-Verzeichnis"],
correct: 1,
color: "#f0f0f0"
},
{
question: "Was macht 'wait'?",
options: ["Wartet 1 Sekunde", "Wartet auf Eingabe", "Wartet auf Background-Jobs", "Pausiert Skript"],
correct: 2,
color: "#f0f0f0"
},
{
question: "Wie zeigt man alle Background-Jobs?",
options: ["ps", "jobs", "bg", "fg"],
correct: 1,
color: "#f0f0f0"
},
{
question: "Was macht 'fg %1'?",
options: ["Beendet Job 1", "Bringt Job 1 in Vordergrund", "Startet Job 1", "Pausiert Job 1"],
correct: 1,
color: "#f0f0f0"
}
]
};
let currentBelt = null;
let currentQuestion = 0;
let answers = [];
let score = 0;
// Belt Selection
document.getElementById('beltSelector').addEventListener('click', (e) => {
const btn = e.target.closest('.belt-btn');
if (!btn) return;
document.querySelectorAll('.belt-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentBelt = btn.dataset.belt;
currentQuestion = 0;
answers = [];
score = 0;
document.getElementById('quizContainer').classList.add('active');
document.getElementById('results').classList.remove('show');
showQuestion();
});
function showQuestion() {
if (!currentBelt) return;
const questions = quizData[currentBelt];
const q = questions[currentQuestion];
document.getElementById('progress').innerHTML = `
<span>Frage ${currentQuestion + 1} von ${questions.length}</span>
<span>🐼 BashPanda beobachtet</span>
`;
const questionHTML = `
<div class="question">
<div class="question-text" style="border-color: ${q.color}">
${currentQuestion + 1}. ${q.question}
</div>
<div class="options">
${q.options.map((opt, idx) => `
<div class="option" data-index="${idx}">
${String.fromCharCode(65 + idx)}. ${opt}
</div>
`).join('')}
</div>
</div>
`;
document.getElementById('questionContainer').innerHTML = questionHTML;
// Restore previous answer if exists
if (answers[currentQuestion] !== undefined) {
const selected = document.querySelector(`[data-index="${answers[currentQuestion]}"]`);
if (selected) selected.classList.add('selected');
}
// Option selection
document.querySelectorAll('.option').forEach(opt => {
opt.addEventListener('click', () => {
document.querySelectorAll('.option').forEach(o => o.classList.remove('selected'));
opt.classList.add('selected');
answers[currentQuestion] = parseInt(opt.dataset.index);
document.getElementById('nextBtn').disabled = false;
});
});
updateButtons();
}
function updateButtons() {
document.getElementById('prevBtn').disabled = currentQuestion === 0;
document.getElementById('nextBtn').disabled = answers[currentQuestion] === undefined;
const questions = quizData[currentBelt];
if (currentQuestion === questions.length - 1) {
document.getElementById('nextBtn').textContent = '🎯 Auswerten';
} else {
document.getElementById('nextBtn').textContent = 'Weiter ➡️';
}
}
document.getElementById('prevBtn').addEventListener('click', () => {
if (currentQuestion > 0) {
currentQuestion--;
showQuestion();
}
});
document.getElementById('nextBtn').addEventListener('click', () => {
const questions = quizData[currentBelt];
if (currentQuestion < questions.length - 1) {
currentQuestion++;
showQuestion();
} else {
showResults();
}
});
function showResults() {
const questions = quizData[currentBelt];
score = 0;
answers.forEach((ans, idx) => {
if (ans === questions[idx].correct) {
score++;
}
});
const percentage = (score / questions.length * 100).toFixed(0);
let verdict, color, emoji;
if (percentage >= 80) {
verdict = "🎉 BESTANDEN! 🎉<br>Der Gürtel gehört dir!";
color = "#32cd32";
emoji = "✅";
} else if (percentage >= 60) {
verdict = "Fast geschafft!<br>Übe noch ein wenig.";
color = "#ffd700";
emoji = "⚠️";
} else {
verdict = "Noch nicht bereit.<br>Lerne weiter, junger Schüler.";
color = "#ff4444";
emoji = "❌";
}
const resultData = {
belt: currentBelt,
score: score,
total: questions.length,
percentage: percentage,
passed: percentage >= 80,
timestamp: new Date().toISOString()
};
document.getElementById('results').innerHTML = `
<h2>${emoji} Gürtelprüfung: ${currentBelt.toUpperCase()}</h2>
<div class="score" style="color: ${color}">
${score} / ${questions.length}
</div>
<div style="font-size: 2em; margin: 20px 0;">
${percentage}%
</div>
<div class="verdict" style="border-color: ${color}">
${verdict}
</div>
<div class="export-info">
<p><strong>📋 Ergebnis exportiert!</strong></p>
<p>Die Ergebnisse wurden in die Zwischenablage kopiert.</p>
<p>Füge sie im Terminal ein für die Auswertung!</p>
<br>
<button class="btn btn-primary" onclick="location.reload()">🔄 Neue Prüfung</button>
</div>
`;
document.getElementById('quizContainer').classList.remove('active');
document.getElementById('results').classList.add('show');
// Copy to clipboard
const exportText = JSON.stringify(resultData, null, 2);
navigator.clipboard.writeText(exportText).then(() => {
console.log('Ergebnisse in Zwischenablage kopiert!');
});
}
</script>
</body>
</html>

628
crumbblocks/bezier.html Normal file
View File

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

View File

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

View File

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

298
crumbblocks/index.html Normal file
View File

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

278
crumbblocks/index_v1.html Normal file
View File

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

2740
crumbblocks/lib/blockly/blockly.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,193 @@
// Do not edit this file; automatically generated.
/* eslint-disable */
;(function(root, factory) {
if (typeof define === 'function' && define.amd) { // AMD
define(["./blockly_compressed.js"], factory);
} else if (typeof exports === 'object') { // Node.js
module.exports = factory(require("./blockly_compressed.js"));
} else { // Script
root.Blockly.libraryBlocks = factory(root.Blockly);
}
}(this, function(__parent__) {
var $=__parent__.__namespace__;
var blocks$$module$build$src$blocks$variables_dynamic=$.createBlockDefinitionsFromJsonArray$$module$build$src$core$common([{type:"variables_get_dynamic",message0:"%1",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"}],output:null,style:"variable_dynamic_blocks",helpUrl:"%{BKY_VARIABLES_GET_HELPURL}",tooltip:"%{BKY_VARIABLES_GET_TOOLTIP}",extensions:["contextMenu_variableDynamicSetterGetter"]},{type:"variables_set_dynamic",message0:"%{BKY_VARIABLES_SET}",args0:[{type:"field_variable",
name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"},{type:"input_value",name:"VALUE"}],previousStatement:null,nextStatement:null,style:"variable_dynamic_blocks",tooltip:"%{BKY_VARIABLES_SET_TOOLTIP}",helpUrl:"%{BKY_VARIABLES_SET_HELPURL}",extensions:["contextMenu_variableDynamicSetterGetter"]}]),CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN$$module$build$src$blocks$variables_dynamic={customContextMenu:function(a){if(!this.isInFlyout){if(this.type==="variables_get_dynamic"){var b="variables_set_dynamic";
var c=$.Msg$$module$build$src$core$msg.VARIABLES_GET_CREATE_SET}else b="variables_get_dynamic",c=$.Msg$$module$build$src$core$msg.VARIABLES_SET_CREATE_GET;var d=this.getField("VAR");b={type:b,fields:{VAR:d.saveState(!0)}};a.push({enabled:this.workspace.remainingCapacity()>0,text:c.replace("%1",d.getText()),callback:$.callbackFactory$$module$build$src$core$contextmenu(this,b)})}else if(this.type==="variables_get_dynamic"||this.type==="variables_get_reporter_dynamic")c={text:$.Msg$$module$build$src$core$msg.RENAME_VARIABLE,
enabled:!0,callback:renameOptionCallbackFactory$$module$build$src$blocks$variables_dynamic(this)},d=this.getField("VAR").getText(),d={text:$.Msg$$module$build$src$core$msg.DELETE_VARIABLE.replace("%1",d),enabled:!0,callback:deleteOptionCallbackFactory$$module$build$src$blocks$variables_dynamic(this)},a.unshift(c),a.unshift(d)},onchange:function(a){a=this.getFieldValue("VAR");a=$.getVariable$$module$build$src$core$variables(this.workspace,a);this.type==="variables_get_dynamic"?this.outputConnection.setCheck(a.getType()):
this.getInput("VALUE").connection.setCheck(a.getType())}},renameOptionCallbackFactory$$module$build$src$blocks$variables_dynamic=function(a){return function(){const b=a.workspace,c=a.getField("VAR").getVariable();$.renameVariable$$module$build$src$core$variables(b,c)}},deleteOptionCallbackFactory$$module$build$src$blocks$variables_dynamic=function(a){return function(){const b=a.getField("VAR").getVariable();b&&$.deleteVariable$$module$build$src$core$variables(b.getWorkspace(),b,a)}};
$.registerMixin$$module$build$src$core$extensions("contextMenu_variableDynamicSetterGetter",CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN$$module$build$src$blocks$variables_dynamic);$.defineBlocks$$module$build$src$core$common(blocks$$module$build$src$blocks$variables_dynamic);var module$build$src$blocks$variables_dynamic={blocks:blocks$$module$build$src$blocks$variables_dynamic};var blocks$$module$build$src$blocks$variables=$.createBlockDefinitionsFromJsonArray$$module$build$src$core$common([{type:"variables_get",message0:"%1",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"}],output:null,style:"variable_blocks",helpUrl:"%{BKY_VARIABLES_GET_HELPURL}",tooltip:"%{BKY_VARIABLES_GET_TOOLTIP}",extensions:["contextMenu_variableSetterGetter"]},{type:"variables_set",message0:"%{BKY_VARIABLES_SET}",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_VARIABLES_DEFAULT_NAME}"},
{type:"input_value",name:"VALUE"}],previousStatement:null,nextStatement:null,style:"variable_blocks",tooltip:"%{BKY_VARIABLES_SET_TOOLTIP}",helpUrl:"%{BKY_VARIABLES_SET_HELPURL}",extensions:["contextMenu_variableSetterGetter"]}]),CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN$$module$build$src$blocks$variables={customContextMenu:function(a){if(!this.isInFlyout){if(this.type==="variables_get"){var b="variables_set";var c=$.Msg$$module$build$src$core$msg.VARIABLES_GET_CREATE_SET}else b="variables_get",
c=$.Msg$$module$build$src$core$msg.VARIABLES_SET_CREATE_GET;var d=this.getField("VAR");b={type:b,fields:{VAR:d.saveState(!0)}};a.push({enabled:this.workspace.remainingCapacity()>0,text:c.replace("%1",d.getText()),callback:$.callbackFactory$$module$build$src$core$contextmenu(this,b)})}else if(this.type==="variables_get"||this.type==="variables_get_reporter")c={text:$.Msg$$module$build$src$core$msg.RENAME_VARIABLE,enabled:!0,callback:renameOptionCallbackFactory$$module$build$src$blocks$variables(this)},
d=this.getField("VAR").getText(),d={text:$.Msg$$module$build$src$core$msg.DELETE_VARIABLE.replace("%1",d),enabled:!0,callback:deleteOptionCallbackFactory$$module$build$src$blocks$variables(this)},a.unshift(c),a.unshift(d)}},renameOptionCallbackFactory$$module$build$src$blocks$variables=function(a){return function(){const b=a.workspace,c=a.getField("VAR").getVariable();$.renameVariable$$module$build$src$core$variables(b,c)}},deleteOptionCallbackFactory$$module$build$src$blocks$variables=function(a){return function(){const b=
a.getField("VAR").getVariable();b&&$.deleteVariable$$module$build$src$core$variables(b.getWorkspace(),b,a)}};$.registerMixin$$module$build$src$core$extensions("contextMenu_variableSetterGetter",CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN$$module$build$src$blocks$variables);$.defineBlocks$$module$build$src$core$common(blocks$$module$build$src$blocks$variables);var module$build$src$blocks$variables={blocks:blocks$$module$build$src$blocks$variables};var blocks$$module$build$src$blocks$text=$.createBlockDefinitionsFromJsonArray$$module$build$src$core$common([{type:"text",message0:"%1",args0:[{type:"field_input",name:"TEXT",text:""}],output:"String",style:"text_blocks",helpUrl:"%{BKY_TEXT_TEXT_HELPURL}",tooltip:"%{BKY_TEXT_TEXT_TOOLTIP}",extensions:["text_quotes","parent_tooltip_when_inline"]},{type:"text_join",message0:"",output:"String",style:"text_blocks",helpUrl:"%{BKY_TEXT_JOIN_HELPURL}",tooltip:"%{BKY_TEXT_JOIN_TOOLTIP}",mutator:"text_join_mutator"},
{type:"text_create_join_container",message0:"%{BKY_TEXT_CREATE_JOIN_TITLE_JOIN} %1 %2",args0:[{type:"input_dummy"},{type:"input_statement",name:"STACK"}],style:"text_blocks",tooltip:"%{BKY_TEXT_CREATE_JOIN_TOOLTIP}",enableContextMenu:!1},{type:"text_create_join_item",message0:"%{BKY_TEXT_CREATE_JOIN_ITEM_TITLE_ITEM}",previousStatement:null,nextStatement:null,style:"text_blocks",tooltip:"%{BKY_TEXT_CREATE_JOIN_ITEM_TOOLTIP}",enableContextMenu:!1},{type:"text_append",message0:"%{BKY_TEXT_APPEND_TITLE}",
args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_TEXT_APPEND_VARIABLE}"},{type:"input_value",name:"TEXT"}],previousStatement:null,nextStatement:null,style:"text_blocks",extensions:["text_append_tooltip"]},{type:"text_length",message0:"%{BKY_TEXT_LENGTH_TITLE}",args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Number",style:"text_blocks",tooltip:"%{BKY_TEXT_LENGTH_TOOLTIP}",helpUrl:"%{BKY_TEXT_LENGTH_HELPURL}"},{type:"text_isEmpty",message0:"%{BKY_TEXT_ISEMPTY_TITLE}",
args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Boolean",style:"text_blocks",tooltip:"%{BKY_TEXT_ISEMPTY_TOOLTIP}",helpUrl:"%{BKY_TEXT_ISEMPTY_HELPURL}"},{type:"text_indexOf",message0:"%{BKY_TEXT_INDEXOF_TITLE}",args0:[{type:"input_value",name:"VALUE",check:"String"},{type:"field_dropdown",name:"END",options:[["%{BKY_TEXT_INDEXOF_OPERATOR_FIRST}","FIRST"],["%{BKY_TEXT_INDEXOF_OPERATOR_LAST}","LAST"]]},{type:"input_value",name:"FIND",check:"String"}],output:"Number",style:"text_blocks",
helpUrl:"%{BKY_TEXT_INDEXOF_HELPURL}",inputsInline:!0,extensions:["text_indexOf_tooltip"]},{type:"text_charAt",message0:"%{BKY_TEXT_CHARAT_TITLE}",args0:[{type:"input_value",name:"VALUE",check:"String"},{type:"field_dropdown",name:"WHERE",options:[["%{BKY_TEXT_CHARAT_FROM_START}","FROM_START"],["%{BKY_TEXT_CHARAT_FROM_END}","FROM_END"],["%{BKY_TEXT_CHARAT_FIRST}","FIRST"],["%{BKY_TEXT_CHARAT_LAST}","LAST"],["%{BKY_TEXT_CHARAT_RANDOM}","RANDOM"]]}],output:"String",style:"text_blocks",helpUrl:"%{BKY_TEXT_CHARAT_HELPURL}",
inputsInline:!0,mutator:"text_charAt_mutator"}]),GET_SUBSTRING_BLOCK$$module$build$src$blocks$text={init:function(){this.WHERE_OPTIONS_1=[[$.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_START_FROM_START,"FROM_START"],[$.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_START_FROM_END,"FROM_END"],[$.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_START_FIRST,"FIRST"]];this.WHERE_OPTIONS_2=[[$.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_END_FROM_START,"FROM_START"],[$.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_END_FROM_END,
"FROM_END"],[$.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_END_LAST,"LAST"]];this.setHelpUrl($.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_HELPURL);this.setStyle("text_blocks");this.appendValueInput("STRING").setCheck("String").appendField($.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_INPUT_IN_TEXT);const a=b=>{const c=$.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",options:this["WHERE_OPTIONS_"+b]});c.setValidator(function(d){const e=this.getValue();d=d===
"FROM_START"||d==="FROM_END";d!==(e==="FROM_START"||e==="FROM_END")&&this.getSourceBlock().updateAt_(b,d)});return c};this.appendDummyInput("WHERE1_INPUT").appendField(a(1),"WHERE1");this.appendDummyInput("AT1");this.appendDummyInput("WHERE2_INPUT").appendField(a(2),"WHERE2");this.appendDummyInput("AT2");$.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_TAIL&&this.appendDummyInput("TAIL").appendField($.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_TAIL);this.setInputsInline(!0);this.setOutput(!0,
"String");this.updateAt_(1,!0);this.updateAt_(2,!0);this.setTooltip($.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_TOOLTIP)},mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");var b=this.getInput("AT1")instanceof $.ValueInput$$module$build$src$core$inputs$value_input;a.setAttribute("at1",`${b}`);b=this.getInput("AT2")instanceof $.ValueInput$$module$build$src$core$inputs$value_input;a.setAttribute("at2",`${b}`);return a},domToMutation:function(a){const b=
a.getAttribute("at1")==="true";a=a.getAttribute("at2")==="true";this.updateAt_(1,b);this.updateAt_(2,a)},updateAt_:function(a,b){this.removeInput("AT"+a);this.removeInput("ORDINAL"+a,!0);b?(this.appendValueInput("AT"+a).setCheck("Number"),$.Msg$$module$build$src$core$msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL"+a).appendField($.Msg$$module$build$src$core$msg.ORDINAL_NUMBER_SUFFIX)):this.appendDummyInput("AT"+a);a===2&&$.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_TAIL&&(this.removeInput("TAIL",
!0),this.appendDummyInput("TAIL").appendField($.Msg$$module$build$src$core$msg.TEXT_GET_SUBSTRING_TAIL));a===1&&(this.moveInputBefore("AT1","WHERE2_INPUT"),this.getInput("ORDINAL1")&&this.moveInputBefore("ORDINAL1","WHERE2_INPUT"))}};blocks$$module$build$src$blocks$text.text_getSubstring=GET_SUBSTRING_BLOCK$$module$build$src$blocks$text;
blocks$$module$build$src$blocks$text.text_changeCase={init:function(){const a=[[$.Msg$$module$build$src$core$msg.TEXT_CHANGECASE_OPERATOR_UPPERCASE,"UPPERCASE"],[$.Msg$$module$build$src$core$msg.TEXT_CHANGECASE_OPERATOR_LOWERCASE,"LOWERCASE"],[$.Msg$$module$build$src$core$msg.TEXT_CHANGECASE_OPERATOR_TITLECASE,"TITLECASE"]];this.setHelpUrl($.Msg$$module$build$src$core$msg.TEXT_CHANGECASE_HELPURL);this.setStyle("text_blocks");this.appendValueInput("TEXT").setCheck("String").appendField($.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",
options:a}),"CASE");this.setOutput(!0,"String");this.setTooltip($.Msg$$module$build$src$core$msg.TEXT_CHANGECASE_TOOLTIP)}};
blocks$$module$build$src$blocks$text.text_trim={init:function(){const a=[[$.Msg$$module$build$src$core$msg.TEXT_TRIM_OPERATOR_BOTH,"BOTH"],[$.Msg$$module$build$src$core$msg.TEXT_TRIM_OPERATOR_LEFT,"LEFT"],[$.Msg$$module$build$src$core$msg.TEXT_TRIM_OPERATOR_RIGHT,"RIGHT"]];this.setHelpUrl($.Msg$$module$build$src$core$msg.TEXT_TRIM_HELPURL);this.setStyle("text_blocks");this.appendValueInput("TEXT").setCheck("String").appendField($.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",options:a}),
"MODE");this.setOutput(!0,"String");this.setTooltip($.Msg$$module$build$src$core$msg.TEXT_TRIM_TOOLTIP)}};blocks$$module$build$src$blocks$text.text_print={init:function(){this.jsonInit({message0:$.Msg$$module$build$src$core$msg.TEXT_PRINT_TITLE,args0:[{type:"input_value",name:"TEXT"}],previousStatement:null,nextStatement:null,style:"text_blocks",tooltip:$.Msg$$module$build$src$core$msg.TEXT_PRINT_TOOLTIP,helpUrl:$.Msg$$module$build$src$core$msg.TEXT_PRINT_HELPURL})}};
var PROMPT_COMMON$$module$build$src$blocks$text={updateType_:function(a){this.outputConnection.setCheck(a==="NUMBER"?"Number":"String")},mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");a.setAttribute("type",this.getFieldValue("TYPE"));return a},domToMutation:function(a){this.updateType_(a.getAttribute("type"))}};
blocks$$module$build$src$blocks$text.text_prompt_ext=Object.assign({},PROMPT_COMMON$$module$build$src$blocks$text,{init:function(){var a=[[$.Msg$$module$build$src$core$msg.TEXT_PROMPT_TYPE_TEXT,"TEXT"],[$.Msg$$module$build$src$core$msg.TEXT_PROMPT_TYPE_NUMBER,"NUMBER"]];this.setHelpUrl($.Msg$$module$build$src$core$msg.TEXT_PROMPT_HELPURL);this.setStyle("text_blocks");a=$.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",options:a});a.setValidator(b=>{this.updateType_(b)});this.appendValueInput("TEXT").appendField(a,
"TYPE");this.setOutput(!0,"String");this.setTooltip(()=>this.getFieldValue("TYPE")==="TEXT"?$.Msg$$module$build$src$core$msg.TEXT_PROMPT_TOOLTIP_TEXT:$.Msg$$module$build$src$core$msg.TEXT_PROMPT_TOOLTIP_NUMBER)}});
blocks$$module$build$src$blocks$text.text_prompt=Object.assign({},PROMPT_COMMON$$module$build$src$blocks$text,{init:function(){this.mixin(QUOTE_IMAGE_MIXIN$$module$build$src$blocks$text);var a=[[$.Msg$$module$build$src$core$msg.TEXT_PROMPT_TYPE_TEXT,"TEXT"],[$.Msg$$module$build$src$core$msg.TEXT_PROMPT_TYPE_NUMBER,"NUMBER"]];this.setHelpUrl($.Msg$$module$build$src$core$msg.TEXT_PROMPT_HELPURL);this.setStyle("text_blocks");a=$.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",options:a});
a.setValidator(b=>{this.updateType_(b)});this.appendDummyInput().appendField(a,"TYPE").appendField(this.newQuote_(!0)).appendField($.fromJson$$module$build$src$core$field_registry({type:"field_input",text:""}),"TEXT").appendField(this.newQuote_(!1));this.setOutput(!0,"String");this.setTooltip(()=>this.getFieldValue("TYPE")==="TEXT"?$.Msg$$module$build$src$core$msg.TEXT_PROMPT_TOOLTIP_TEXT:$.Msg$$module$build$src$core$msg.TEXT_PROMPT_TOOLTIP_NUMBER)}});
blocks$$module$build$src$blocks$text.text_count={init:function(){this.jsonInit({message0:$.Msg$$module$build$src$core$msg.TEXT_COUNT_MESSAGE0,args0:[{type:"input_value",name:"SUB",check:"String"},{type:"input_value",name:"TEXT",check:"String"}],output:"Number",inputsInline:!0,style:"text_blocks",tooltip:$.Msg$$module$build$src$core$msg.TEXT_COUNT_TOOLTIP,helpUrl:$.Msg$$module$build$src$core$msg.TEXT_COUNT_HELPURL})}};
blocks$$module$build$src$blocks$text.text_replace={init:function(){this.jsonInit({message0:$.Msg$$module$build$src$core$msg.TEXT_REPLACE_MESSAGE0,args0:[{type:"input_value",name:"FROM",check:"String"},{type:"input_value",name:"TO",check:"String"},{type:"input_value",name:"TEXT",check:"String"}],output:"String",inputsInline:!0,style:"text_blocks",tooltip:$.Msg$$module$build$src$core$msg.TEXT_REPLACE_TOOLTIP,helpUrl:$.Msg$$module$build$src$core$msg.TEXT_REPLACE_HELPURL})}};
blocks$$module$build$src$blocks$text.text_reverse={init:function(){this.jsonInit({message0:$.Msg$$module$build$src$core$msg.TEXT_REVERSE_MESSAGE0,args0:[{type:"input_value",name:"TEXT",check:"String"}],output:"String",inputsInline:!0,style:"text_blocks",tooltip:$.Msg$$module$build$src$core$msg.TEXT_REVERSE_TOOLTIP,helpUrl:$.Msg$$module$build$src$core$msg.TEXT_REVERSE_HELPURL})}};
var QUOTE_IMAGE_MIXIN$$module$build$src$blocks$text={QUOTE_IMAGE_LEFT_DATAURI:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAn0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMfz9AylsaRRgGzvZAAAAAElFTkSuQmCC",QUOTE_IMAGE_RIGHT_DATAURI:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAqUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhggONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvBO3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5AoslLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg==",
QUOTE_IMAGE_WIDTH:12,QUOTE_IMAGE_HEIGHT:12,quoteField_:function(a){for(let b=0,c;c=this.inputList[b];b++)for(let d=0,e;e=c.fieldRow[d];d++)if(a===e.name){c.insertFieldAt(d,this.newQuote_(!0));c.insertFieldAt(d+2,this.newQuote_(!1));return}console.warn('field named "'+a+'" not found in '+this.toDevString())},newQuote_:function(a){a=this.RTL?!a:a;return $.fromJson$$module$build$src$core$field_registry({type:"field_image",src:a?this.QUOTE_IMAGE_LEFT_DATAURI:this.QUOTE_IMAGE_RIGHT_DATAURI,width:this.QUOTE_IMAGE_WIDTH,
height:this.QUOTE_IMAGE_HEIGHT,alt:a?"\u201c":"\u201d"})}},QUOTES_EXTENSION$$module$build$src$blocks$text=function(){this.mixin(QUOTE_IMAGE_MIXIN$$module$build$src$blocks$text);this.quoteField_("TEXT")},JOIN_MUTATOR_MIXIN$$module$build$src$blocks$text={itemCount_:0,mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");a.setAttribute("items",`${this.itemCount_}`);return a},domToMutation:function(a){this.itemCount_=parseInt(a.getAttribute("items"),10);this.updateShape_()},
saveExtraState:function(){return{itemCount:this.itemCount_}},loadExtraState:function(a){this.itemCount_=a.itemCount;this.updateShape_()},decompose:function(a){const b=a.newBlock("text_create_join_container");b.initSvg();let c=b.getInput("STACK").connection;for(let d=0;d<this.itemCount_;d++){const e=a.newBlock("text_create_join_item");e.initSvg();c.connect(e.previousConnection);c=e.nextConnection}return b},compose:function(a){var b=a.getInputTargetBlock("STACK");for(a=[];b;)b.isInsertionMarker()||
a.push(b.valueConnection_),b=b.getNextBlock();for(b=0;b<this.itemCount_;b++){const c=this.getInput("ADD"+b).connection.targetConnection;c&&!a.includes(c)&&c.disconnect()}this.itemCount_=a.length;this.updateShape_();for(b=0;b<this.itemCount_;b++){let c;(c=a[b])==null||c.reconnect(this,"ADD"+b)}},saveConnections:function(a){a=a.getInputTargetBlock("STACK");let b=0;for(;a;){if(a.isInsertionMarker()){a=a.getNextBlock();continue}const c=this.getInput("ADD"+b);a.valueConnection_=c&&c.connection.targetConnection;
a=a.getNextBlock();b++}},updateShape_:function(){this.itemCount_&&this.getInput("EMPTY")?this.removeInput("EMPTY"):this.itemCount_||this.getInput("EMPTY")||this.appendDummyInput("EMPTY").appendField(this.newQuote_(!0)).appendField(this.newQuote_(!1));for(var a=0;a<this.itemCount_;a++)if(!this.getInput("ADD"+a)){const b=this.appendValueInput("ADD"+a).setAlign($.Align$$module$build$src$core$inputs$align.RIGHT);a===0&&b.appendField($.Msg$$module$build$src$core$msg.TEXT_JOIN_TITLE_CREATEWITH)}for(a=this.itemCount_;this.getInput("ADD"+
a);a++)this.removeInput("ADD"+a)}},JOIN_EXTENSION$$module$build$src$blocks$text=function(){this.mixin(QUOTE_IMAGE_MIXIN$$module$build$src$blocks$text);this.itemCount_=2;this.updateShape_();this.setMutator(new $.MutatorIcon$$module$build$src$core$icons$mutator_icon(["text_create_join_item"],this))};$.register$$module$build$src$core$extensions("text_append_tooltip",$.buildTooltipWithFieldText$$module$build$src$core$extensions("%{BKY_TEXT_APPEND_TOOLTIP}","VAR"));
var INDEXOF_TOOLTIP_EXTENSION$$module$build$src$blocks$text=function(){this.setTooltip(()=>$.Msg$$module$build$src$core$msg.TEXT_INDEXOF_TOOLTIP.replace("%1",this.workspace.options.oneBasedIndex?"0":"-1"))},CHARAT_MUTATOR_MIXIN$$module$build$src$blocks$text={isAt_:!1,mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");a.setAttribute("at",`${this.isAt_}`);return a},domToMutation:function(a){a=a.getAttribute("at")!=="false";this.updateAt_(a)},updateAt_:function(a){this.removeInput("AT",
!0);this.removeInput("ORDINAL",!0);a&&(this.appendValueInput("AT").setCheck("Number"),$.Msg$$module$build$src$core$msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL").appendField($.Msg$$module$build$src$core$msg.ORDINAL_NUMBER_SUFFIX));$.Msg$$module$build$src$core$msg.TEXT_CHARAT_TAIL&&(this.removeInput("TAIL",!0),this.appendDummyInput("TAIL").appendField($.Msg$$module$build$src$core$msg.TEXT_CHARAT_TAIL));this.isAt_=a}},CHARAT_EXTENSION$$module$build$src$blocks$text=function(){this.getField("WHERE").setValidator(function(a){a=
a==="FROM_START"||a==="FROM_END";const b=this.getSourceBlock();a!==b.isAt_&&b.updateAt_(a)});this.updateAt_(!0);this.setTooltip(()=>{var a=this.getFieldValue("WHERE");let b=$.Msg$$module$build$src$core$msg.TEXT_CHARAT_TOOLTIP;(a==="FROM_START"||a==="FROM_END")&&(a=a==="FROM_START"?$.Msg$$module$build$src$core$msg.LISTS_INDEX_FROM_START_TOOLTIP:$.Msg$$module$build$src$core$msg.LISTS_INDEX_FROM_END_TOOLTIP)&&(b+=" "+a.replace("%1",this.workspace.options.oneBasedIndex?"#1":"#0"));return b})};
$.register$$module$build$src$core$extensions("text_indexOf_tooltip",INDEXOF_TOOLTIP_EXTENSION$$module$build$src$blocks$text);$.register$$module$build$src$core$extensions("text_quotes",QUOTES_EXTENSION$$module$build$src$blocks$text);$.registerMixin$$module$build$src$core$extensions("quote_image_mixin",QUOTE_IMAGE_MIXIN$$module$build$src$blocks$text);$.registerMutator$$module$build$src$core$extensions("text_join_mutator",JOIN_MUTATOR_MIXIN$$module$build$src$blocks$text,JOIN_EXTENSION$$module$build$src$blocks$text);
$.registerMutator$$module$build$src$core$extensions("text_charAt_mutator",CHARAT_MUTATOR_MIXIN$$module$build$src$blocks$text,CHARAT_EXTENSION$$module$build$src$blocks$text);$.defineBlocks$$module$build$src$core$common(blocks$$module$build$src$blocks$text);var module$build$src$blocks$text={blocks:blocks$$module$build$src$blocks$text};var blocks$$module$build$src$blocks$procedures={},PROCEDURE_DEF_COMMON$$module$build$src$blocks$procedures={setStatements_:function(a){this.hasStatements_!==a&&(a?(this.appendStatementInput("STACK").appendField($.Msg$$module$build$src$core$msg.PROCEDURES_DEFNORETURN_DO),this.getInput("RETURN")&&this.moveInputBefore("STACK","RETURN")):this.removeInput("STACK",!0),this.hasStatements_=a)},updateParams_:function(){let a="";this.arguments_.length&&(a=$.Msg$$module$build$src$core$msg.PROCEDURES_BEFORE_PARAMS+
" "+this.arguments_.join(", "));$.disable$$module$build$src$core$events$utils();try{this.setFieldValue(a,"PARAMS")}finally{$.enable$$module$build$src$core$events$utils()}},mutationToDom:function(a){const b=$.createElement$$module$build$src$core$utils$xml("mutation");a&&b.setAttribute("name",this.getFieldValue("NAME"));for(let c=0;c<this.argumentVarModels_.length;c++){const d=$.createElement$$module$build$src$core$utils$xml("arg"),e=this.argumentVarModels_[c];d.setAttribute("name",e.getName());d.setAttribute("varid",
e.getId());a&&this.paramIds_&&d.setAttribute("paramId",this.paramIds_[c]);b.appendChild(d)}this.hasStatements_||b.setAttribute("statements","false");return b},domToMutation:function(a){this.arguments_=[];this.argumentVarModels_=[];for(let c=0,d;d=a.childNodes[c];c++)if(d.nodeName.toLowerCase()==="arg"){var b=d;const e=b.getAttribute("name");b=b.getAttribute("varid")||b.getAttribute("varId");this.arguments_.push(e);b=$.getOrCreateVariablePackage$$module$build$src$core$variables(this.workspace,b,e,
"");b!==null?this.argumentVarModels_.push(b):console.log(`Failed to create a variable named "${e}", ignoring.`)}this.updateParams_();$.mutateCallers$$module$build$src$core$procedures(this);this.setStatements_(a.getAttribute("statements")!=="false")},saveExtraState:function(){if(!this.argumentVarModels_.length&&this.hasStatements_)return null;const a=Object.create(null);if(this.argumentVarModels_.length){a.params=[];for(let b=0;b<this.argumentVarModels_.length;b++)a.params.push({name:this.argumentVarModels_[b].getName(),
id:this.argumentVarModels_[b].getId()})}this.hasStatements_||(a.hasStatements=!1);return a},loadExtraState:function(a){this.arguments_=[];this.argumentVarModels_=[];if(a.params)for(let c=0;c<a.params.length;c++){var b=a.params[c];b=$.getOrCreateVariablePackage$$module$build$src$core$variables(this.workspace,b.id,b.name,"");this.arguments_.push(b.getName());this.argumentVarModels_.push(b)}this.updateParams_();$.mutateCallers$$module$build$src$core$procedures(this);this.setStatements_(a.hasStatements===
!1?!1:!0)},decompose:function(a){const b=$.createElement$$module$build$src$core$utils$xml("block");b.setAttribute("type","procedures_mutatorcontainer");var c=$.createElement$$module$build$src$core$utils$xml("statement");c.setAttribute("name","STACK");b.appendChild(c);for(let e=0;e<this.arguments_.length;e++){const f=$.createElement$$module$build$src$core$utils$xml("block");f.setAttribute("type","procedures_mutatorarg");var d=$.createElement$$module$build$src$core$utils$xml("field");d.setAttribute("name",
"NAME");const g=$.createTextNode$$module$build$src$core$utils$xml(this.arguments_[e]);d.appendChild(g);f.appendChild(d);d=$.createElement$$module$build$src$core$utils$xml("next");f.appendChild(d);c.appendChild(f);c=d}a=$.domToBlock$$module$build$src$core$xml(b,a);this.type==="procedures_defreturn"?a.setFieldValue(this.hasStatements_,"STATEMENTS"):a.removeInput("STATEMENT_INPUT");$.mutateCallers$$module$build$src$core$procedures(this);return a},compose:function(a){this.arguments_=[];this.paramIds_=
[];this.argumentVarModels_=[];let b=a.getInputTargetBlock("STACK");for(;b&&!b.isInsertionMarker();){var c=b.getFieldValue("NAME");this.arguments_.push(c);c=this.workspace.getVariable(c,"");this.argumentVarModels_.push(c);this.paramIds_.push(b.id);b=b.nextConnection&&b.nextConnection.targetBlock()}this.updateParams_();$.mutateCallers$$module$build$src$core$procedures(this);a=a.getFieldValue("STATEMENTS");if(a!==null&&(a=a==="TRUE",this.hasStatements_!==a))if(a){this.setStatements_(!0);var d;(d=this.statementConnection_)==
null||d.reconnect(this,"STACK");this.statementConnection_=null}else{d=this.getInput("STACK").connection;if(this.statementConnection_=d.targetConnection)d=d.targetBlock(),d.unplug(),d.bumpNeighbours();this.setStatements_(!1)}},getVars:function(){return this.arguments_},getVarModels:function(){return this.argumentVarModels_},renameVarById:function(a,b){var c=this.workspace.getVariableById(a);if(c.getType()===""){c=c.getName();b=this.workspace.getVariableById(b);var d=!1;for(let e=0;e<this.argumentVarModels_.length;e++)this.argumentVarModels_[e].getId()===
a&&(this.arguments_[e]=b.getName(),this.argumentVarModels_[e]=b,d=!0);d&&(this.displayRenamedVar_(c,b.getName()),$.mutateCallers$$module$build$src$core$procedures(this))}},updateVarName:function(a){const b=a.getName();let c=!1,d;for(let e=0;e<this.argumentVarModels_.length;e++)this.argumentVarModels_[e].getId()===a.getId()&&(d=this.arguments_[e],this.arguments_[e]=b,c=!0);c&&(this.displayRenamedVar_(d,b),$.mutateCallers$$module$build$src$core$procedures(this))},displayRenamedVar_:function(a,b){this.updateParams_();
var c=this.getIcon($.MutatorIcon$$module$build$src$core$icons$mutator_icon.TYPE);if(c&&c.bubbleIsVisible()){c=c.getWorkspace().getAllBlocks(!1);for(let d=0,e;e=c[d];d++)e.type==="procedures_mutatorarg"&&$.Names$$module$build$src$core$names.equals(a,e.getFieldValue("NAME"))&&e.setFieldValue(b,"NAME")}},customContextMenu:function(a){if(!this.isInFlyout){var b=this.getFieldValue("NAME"),c={type:this.callType_,extraState:{name:b,params:this.arguments_}};a.push({enabled:!0,text:$.Msg$$module$build$src$core$msg.PROCEDURES_CREATE_DO.replace("%1",
b),callback:$.callbackFactory$$module$build$src$core$contextmenu(this,c)});if(!this.isCollapsed())for(b=0;b<this.argumentVarModels_.length;b++){c=this.argumentVarModels_[b];const d={type:"variables_get",fields:{VAR:{name:c.getName(),id:c.getId(),type:c.getType()}}};a.push({enabled:!0,text:$.Msg$$module$build$src$core$msg.VARIABLES_SET_CREATE_GET.replace("%1",c.getName()),callback:$.callbackFactory$$module$build$src$core$contextmenu(this,d)})}}}};
blocks$$module$build$src$blocks$procedures.procedures_defnoreturn=Object.assign({},PROCEDURE_DEF_COMMON$$module$build$src$blocks$procedures,{init:function(){var a=$.findLegalName$$module$build$src$core$procedures("",this);a=$.fromJson$$module$build$src$core$field_registry({type:"field_input",text:a});a.setValidator($.rename$$module$build$src$core$procedures);a.setSpellcheck(!1);this.appendDummyInput().appendField($.Msg$$module$build$src$core$msg.PROCEDURES_DEFNORETURN_TITLE).appendField(a,"NAME").appendField("",
"PARAMS");this.setMutator(new $.MutatorIcon$$module$build$src$core$icons$mutator_icon(["procedures_mutatorarg"],this));(this.workspace.options.comments||this.workspace.options.parentWorkspace&&this.workspace.options.parentWorkspace.options.comments)&&$.Msg$$module$build$src$core$msg.PROCEDURES_DEFNORETURN_COMMENT&&this.setCommentText($.Msg$$module$build$src$core$msg.PROCEDURES_DEFNORETURN_COMMENT);this.setStyle("procedure_blocks");this.setTooltip($.Msg$$module$build$src$core$msg.PROCEDURES_DEFNORETURN_TOOLTIP);
this.setHelpUrl($.Msg$$module$build$src$core$msg.PROCEDURES_DEFNORETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.setStatements_(!0);this.statementConnection_=null},getProcedureDef:function(){return[this.getFieldValue("NAME"),this.arguments_,!1]},callType_:"procedures_callnoreturn"});
blocks$$module$build$src$blocks$procedures.procedures_defreturn=Object.assign({},PROCEDURE_DEF_COMMON$$module$build$src$blocks$procedures,{init:function(){var a=$.findLegalName$$module$build$src$core$procedures("",this);a=$.fromJson$$module$build$src$core$field_registry({type:"field_input",text:a});a.setValidator($.rename$$module$build$src$core$procedures);a.setSpellcheck(!1);this.appendDummyInput().appendField($.Msg$$module$build$src$core$msg.PROCEDURES_DEFRETURN_TITLE).appendField(a,"NAME").appendField("",
"PARAMS");this.appendValueInput("RETURN").setAlign($.Align$$module$build$src$core$inputs$align.RIGHT).appendField($.Msg$$module$build$src$core$msg.PROCEDURES_DEFRETURN_RETURN);this.setMutator(new $.MutatorIcon$$module$build$src$core$icons$mutator_icon(["procedures_mutatorarg"],this));(this.workspace.options.comments||this.workspace.options.parentWorkspace&&this.workspace.options.parentWorkspace.options.comments)&&$.Msg$$module$build$src$core$msg.PROCEDURES_DEFRETURN_COMMENT&&this.setCommentText($.Msg$$module$build$src$core$msg.PROCEDURES_DEFRETURN_COMMENT);
this.setStyle("procedure_blocks");this.setTooltip($.Msg$$module$build$src$core$msg.PROCEDURES_DEFRETURN_TOOLTIP);this.setHelpUrl($.Msg$$module$build$src$core$msg.PROCEDURES_DEFRETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.setStatements_(!0);this.statementConnection_=null},getProcedureDef:function(){return[this.getFieldValue("NAME"),this.arguments_,!0]},callType_:"procedures_callreturn"});
var PROCEDURES_MUTATORCONTAINER$$module$build$src$blocks$procedures={init:function(){this.appendDummyInput().appendField($.Msg$$module$build$src$core$msg.PROCEDURES_MUTATORCONTAINER_TITLE);this.appendStatementInput("STACK");this.appendDummyInput("STATEMENT_INPUT").appendField($.Msg$$module$build$src$core$msg.PROCEDURES_ALLOW_STATEMENTS).appendField($.fromJson$$module$build$src$core$field_registry({type:"field_checkbox",checked:!0}),"STATEMENTS");this.setStyle("procedure_blocks");this.setTooltip($.Msg$$module$build$src$core$msg.PROCEDURES_MUTATORCONTAINER_TOOLTIP);
this.contextMenu=!1}};blocks$$module$build$src$blocks$procedures.procedures_mutatorcontainer=PROCEDURES_MUTATORCONTAINER$$module$build$src$blocks$procedures;
var ProcedureArgumentField$$module$build$src$blocks$procedures=class extends $.FieldTextInput$$module$build$src$core$field_textinput{constructor(){super(...arguments);this.editingInteractively=!1}showEditor_(a){super.showEditor_(a);this.editingInteractively=!0;this.editingVariable=void 0}onFinishEditing_(a){super.onFinishEditing_(a);this.editingInteractively=!1}},PROCEDURES_MUTATORARGUMENT$$module$build$src$blocks$procedures={init:function(){const a=new ProcedureArgumentField$$module$build$src$blocks$procedures($.DEFAULT_ARG$$module$build$src$core$procedures,
this.validator_);this.appendDummyInput().appendField($.Msg$$module$build$src$core$msg.PROCEDURES_MUTATORARG_TITLE).appendField(a,"NAME");this.setPreviousStatement(!0);this.setNextStatement(!0);this.setStyle("procedure_blocks");this.setTooltip($.Msg$$module$build$src$core$msg.PROCEDURES_MUTATORARG_TOOLTIP);this.contextMenu=!1},validator_:function(a){var b=this.getSourceBlock();const c=b.workspace.getRootWorkspace();a=a.replace(/[\s\xa0]+/g," ").replace(/^ | $/g,"");if(!a)return null;const d=(b.workspace.targetWorkspace||
b.workspace).getAllBlocks(!1),e=a.toLowerCase();for(let f=0;f<d.length;f++){if(d[f].id===this.getSourceBlock().id)continue;const g=d[f].getFieldValue("NAME");if(g&&g.toLowerCase()===e)return null}if(b.isInFlyout)return a;(b=c.getVariable(a,""))&&b.getName()!==a&&c.renameVariableById(b.getId(),a);b||(this.editingInteractively?this.editingVariable?c.renameVariableById(this.editingVariable.getId(),a):this.editingVariable=c.createVariable(a,""):c.createVariable(a,""));return a}};
blocks$$module$build$src$blocks$procedures.procedures_mutatorarg=PROCEDURES_MUTATORARGUMENT$$module$build$src$blocks$procedures;
var DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON$$module$build$src$blocks$procedures="DISABLED_PROCEDURE_DEFINITION",PROCEDURE_CALL_COMMON$$module$build$src$blocks$procedures={getProcedureCall:function(){return this.getFieldValue("NAME")},renameProcedure:function(a,b){$.Names$$module$build$src$core$names.equals(a,this.getProcedureCall())&&(this.setFieldValue(b,"NAME"),this.setTooltip((this.outputConnection?$.Msg$$module$build$src$core$msg.PROCEDURES_CALLRETURN_TOOLTIP:$.Msg$$module$build$src$core$msg.PROCEDURES_CALLNORETURN_TOOLTIP).replace("%1",
b)))},setProcedureParameters_:function(a,b){var c=$.getDefinition$$module$build$src$core$procedures(this.getProcedureCall(),this.workspace);(c=(c=c&&c.getIcon($.MutatorIcon$$module$build$src$core$icons$mutator_icon.TYPE))&&c.bubbleIsVisible())?this.setCollapsed(!1):(this.quarkConnections_={},this.quarkIds_=null);if(a.join("\n")===this.arguments_.join("\n"))this.quarkIds_=b;else{if(b.length!==a.length)throw RangeError("paramNames and paramIds must be the same length.");this.quarkIds_||(this.quarkConnections_=
{},this.quarkIds_=[]);for(let e=0;e<this.arguments_.length;e++){var d=this.getInput("ARG"+e);d&&(d=d.connection.targetConnection,this.quarkConnections_[this.quarkIds_[e]]=d,c&&d&&!b.includes(this.quarkIds_[e])&&(d.disconnect(),d.getSourceBlock().bumpNeighbours()))}this.arguments_=[].concat(a);this.argumentVarModels_=[];for(a=0;a<this.arguments_.length;a++)c=$.getOrCreateVariablePackage$$module$build$src$core$variables(this.workspace,null,this.arguments_[a],""),this.argumentVarModels_.push(c);this.updateShape_();
if(this.quarkIds_=b)for(b=0;b<this.arguments_.length;b++)if(a=this.quarkIds_[b],a in this.quarkConnections_){let e;((e=this.quarkConnections_[a])==null?0:e.reconnect(this,"ARG"+b))||delete this.quarkConnections_[a]}}},updateShape_:function(){for(var a=0;a<this.arguments_.length;a++){var b=this.getField("ARGNAME"+a);if(b){$.disable$$module$build$src$core$events$utils();try{b.setValue(this.arguments_[a])}finally{$.enable$$module$build$src$core$events$utils()}}else b=$.fromJson$$module$build$src$core$field_registry({type:"field_label",
text:this.arguments_[a]}),this.appendValueInput("ARG"+a).setAlign($.Align$$module$build$src$core$inputs$align.RIGHT).appendField(b,"ARGNAME"+a)}for(a=this.arguments_.length;this.getInput("ARG"+a);a++)this.removeInput("ARG"+a);(a=this.getInput("TOPROW"))&&(this.arguments_.length?this.getField("WITH")||a.appendField($.Msg$$module$build$src$core$msg.PROCEDURES_CALL_BEFORE_PARAMS,"WITH"):this.getField("WITH")&&a.removeField("WITH"))},mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");
a.setAttribute("name",this.getProcedureCall());for(let b=0;b<this.arguments_.length;b++){const c=$.createElement$$module$build$src$core$utils$xml("arg");c.setAttribute("name",this.arguments_[b]);a.appendChild(c)}return a},domToMutation:function(a){var b=a.getAttribute("name");this.renameProcedure(this.getProcedureCall(),b);b=[];const c=[];for(let d=0,e;e=a.childNodes[d];d++)e.nodeName.toLowerCase()==="arg"&&(b.push(e.getAttribute("name")),c.push(e.getAttribute("paramId")));this.setProcedureParameters_(b,
c)},saveExtraState:function(){const a=Object.create(null);a.name=this.getProcedureCall();this.arguments_.length&&(a.params=this.arguments_);return a},loadExtraState:function(a){this.renameProcedure(this.getProcedureCall(),a.name);if(a=a.params){const b=[];b.length=a.length;b.fill(null);this.setProcedureParameters_(a,b)}},getVars:function(){return this.arguments_},getVarModels:function(){return this.argumentVarModels_},onchange:function(a){if(this.workspace&&!this.workspace.isFlyout&&a.recordUndo)if(a.type===
$.BLOCK_CREATE$$module$build$src$core$events$events&&a.ids.includes(this.id)){var b=this.getProcedureCall(),c=$.getDefinition$$module$build$src$core$procedures(b,this.workspace);!c||c.type===this.defType_&&JSON.stringify(c.getVars())===JSON.stringify(this.arguments_)||(c=null);if(c)c.isEnabled()||(this.setDisabledReason(!0,DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON$$module$build$src$blocks$procedures),this.setWarningText($.Msg$$module$build$src$core$msg.PROCEDURES_CALL_DISABLED_DEF_WARNING.replace("%1",
b)));else{$.setGroup$$module$build$src$core$events$utils(a.group);a=$.createElement$$module$build$src$core$utils$xml("xml");b=$.createElement$$module$build$src$core$utils$xml("block");b.setAttribute("type",this.defType_);c=this.getRelativeToSurfaceXY();var d=c.y+$.config$$module$build$src$core$config.snapRadius*2;b.setAttribute("x",`${c.x+$.config$$module$build$src$core$config.snapRadius*(this.RTL?-1:1)}`);b.setAttribute("y",`${d}`);c=this.mutationToDom();b.appendChild(c);c=$.createElement$$module$build$src$core$utils$xml("field");
c.setAttribute("name","NAME");d=this.getProcedureCall();const e=$.findLegalName$$module$build$src$core$procedures(d,this);d!==e&&this.renameProcedure(d,e);c.appendChild($.createTextNode$$module$build$src$core$utils$xml(d));b.appendChild(c);a.appendChild(b);$.domToWorkspace$$module$build$src$core$xml(a,this.workspace);$.setGroup$$module$build$src$core$events$utils(!1)}}else a.type===$.BLOCK_DELETE$$module$build$src$core$events$events?(b=this.getProcedureCall(),$.getDefinition$$module$build$src$core$procedures(b,
this.workspace)||($.setGroup$$module$build$src$core$events$utils(a.group),this.dispose(!0),$.setGroup$$module$build$src$core$events$utils(!1))):a.type===$.BLOCK_CHANGE$$module$build$src$core$events$events&&a.element==="disabled"&&(b=this.getProcedureCall(),(d=$.getDefinition$$module$build$src$core$procedures(b,this.workspace))&&d.id===a.blockId&&((c=$.getGroup$$module$build$src$core$events$utils())&&console.log("Saw an existing group while responding to a definition change"),$.setGroup$$module$build$src$core$events$utils(a.group),
a=d.isEnabled(),this.setDisabledReason(!a,DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON$$module$build$src$blocks$procedures),this.setWarningText(a?null:$.Msg$$module$build$src$core$msg.PROCEDURES_CALL_DISABLED_DEF_WARNING.replace("%1",b)),$.setGroup$$module$build$src$core$events$utils(c)))},customContextMenu:function(a){if(this.workspace.isMovable()){var b=this.getProcedureCall(),c=this.workspace;a.push({enabled:!0,text:$.Msg$$module$build$src$core$msg.PROCEDURES_HIGHLIGHT_DEF,callback:function(){const d=
$.getDefinition$$module$build$src$core$procedures(b,c);d&&(c.centerOnBlock(d.id),$.getFocusManager$$module$build$src$core$focus_manager().focusNode(d))}})}}};
blocks$$module$build$src$blocks$procedures.procedures_callnoreturn=Object.assign({},PROCEDURE_CALL_COMMON$$module$build$src$blocks$procedures,{init:function(){this.appendDummyInput("TOPROW").appendField("","NAME");this.setPreviousStatement(!0);this.setNextStatement(!0);this.setStyle("procedure_blocks");this.setHelpUrl($.Msg$$module$build$src$core$msg.PROCEDURES_CALLNORETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.quarkConnections_={};this.quarkIds_=null},defType_:"procedures_defnoreturn"});
blocks$$module$build$src$blocks$procedures.procedures_callreturn=Object.assign({},PROCEDURE_CALL_COMMON$$module$build$src$blocks$procedures,{init:function(){this.appendDummyInput("TOPROW").appendField("","NAME");this.setOutput(!0);this.setStyle("procedure_blocks");this.setHelpUrl($.Msg$$module$build$src$core$msg.PROCEDURES_CALLRETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.quarkConnections_={};this.quarkIds_=null},defType_:"procedures_defreturn"});
var UNPARENTED_IFRETURN_DISABLED_REASON$$module$build$src$blocks$procedures="UNPARENTED_IFRETURN",PROCEDURES_IFRETURN$$module$build$src$blocks$procedures={init:function(){this.appendValueInput("CONDITION").setCheck("Boolean").appendField($.Msg$$module$build$src$core$msg.CONTROLS_IF_MSG_IF);this.appendValueInput("VALUE").appendField($.Msg$$module$build$src$core$msg.PROCEDURES_DEFRETURN_RETURN);this.setInputsInline(!0);this.setPreviousStatement(!0);this.setNextStatement(!0);this.setStyle("procedure_blocks");
this.setTooltip($.Msg$$module$build$src$core$msg.PROCEDURES_IFRETURN_TOOLTIP);this.setHelpUrl($.Msg$$module$build$src$core$msg.PROCEDURES_IFRETURN_HELPURL);this.hasReturnValue_=!0},mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");a.setAttribute("value",String(Number(this.hasReturnValue_)));return a},domToMutation:function(a){this.hasReturnValue_=a.getAttribute("value")==="1";this.hasReturnValue_||(this.removeInput("VALUE"),this.appendDummyInput("VALUE").appendField($.Msg$$module$build$src$core$msg.PROCEDURES_DEFRETURN_RETURN))},
onchange:function(a){if(!(this.workspace.isDragging&&this.workspace.isDragging()||a.type!==$.BLOCK_MOVE$$module$build$src$core$events$events&&a.type!==$.BLOCK_CREATE$$module$build$src$core$events$events)){a=!1;var b=this;do{if(this.FUNCTION_TYPES.includes(b.type)){a=!0;break}b=b.getSurroundParent()}while(b);a?(b.type==="procedures_defnoreturn"&&this.hasReturnValue_?(this.removeInput("VALUE"),this.appendDummyInput("VALUE").appendField($.Msg$$module$build$src$core$msg.PROCEDURES_DEFRETURN_RETURN),this.hasReturnValue_=
!1):b.type!=="procedures_defreturn"||this.hasReturnValue_||(this.removeInput("VALUE"),this.appendValueInput("VALUE").appendField($.Msg$$module$build$src$core$msg.PROCEDURES_DEFRETURN_RETURN),this.hasReturnValue_=!0),this.setWarningText(null)):this.setWarningText($.Msg$$module$build$src$core$msg.PROCEDURES_IFRETURN_WARNING);if(!this.isInFlyout)try{$.setRecordUndo$$module$build$src$core$events$utils(!1),this.setDisabledReason(!a,UNPARENTED_IFRETURN_DISABLED_REASON$$module$build$src$blocks$procedures)}finally{$.setRecordUndo$$module$build$src$core$events$utils(!0)}}},
FUNCTION_TYPES:["procedures_defnoreturn","procedures_defreturn"]};blocks$$module$build$src$blocks$procedures.procedures_ifreturn=PROCEDURES_IFRETURN$$module$build$src$blocks$procedures;$.defineBlocks$$module$build$src$core$common(blocks$$module$build$src$blocks$procedures);var module$build$src$blocks$procedures={blocks:blocks$$module$build$src$blocks$procedures};var blocks$$module$build$src$blocks$math=$.createBlockDefinitionsFromJsonArray$$module$build$src$core$common([{type:"math_number",message0:"%1",args0:[{type:"field_number",name:"NUM",value:0}],output:"Number",helpUrl:"%{BKY_MATH_NUMBER_HELPURL}",style:"math_blocks",tooltip:"%{BKY_MATH_NUMBER_TOOLTIP}",extensions:["parent_tooltip_when_inline"]},{type:"math_arithmetic",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A",check:"Number"},{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ADDITION_SYMBOL}",
"ADD"],["%{BKY_MATH_SUBTRACTION_SYMBOL}","MINUS"],["%{BKY_MATH_MULTIPLICATION_SYMBOL}","MULTIPLY"],["%{BKY_MATH_DIVISION_SYMBOL}","DIVIDE"],["%{BKY_MATH_POWER_SYMBOL}","POWER"]]},{type:"input_value",name:"B",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ARITHMETIC_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_single",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_SINGLE_OP_ROOT}","ROOT"],["%{BKY_MATH_SINGLE_OP_ABSOLUTE}",
"ABS"],["-","NEG"],["ln","LN"],["log10","LOG10"],["e^","EXP"],["10^","POW10"]]},{type:"input_value",name:"NUM",check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_SINGLE_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_trig",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_TRIG_SIN}","SIN"],["%{BKY_MATH_TRIG_COS}","COS"],["%{BKY_MATH_TRIG_TAN}","TAN"],["%{BKY_MATH_TRIG_ASIN}","ASIN"],["%{BKY_MATH_TRIG_ACOS}","ACOS"],["%{BKY_MATH_TRIG_ATAN}",
"ATAN"]]},{type:"input_value",name:"NUM",check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_TRIG_HELPURL}",extensions:["math_op_tooltip"]},{type:"math_constant",message0:"%1",args0:[{type:"field_dropdown",name:"CONSTANT",options:[["\u03c0","PI"],["e","E"],["\u03c6","GOLDEN_RATIO"],["sqrt(2)","SQRT2"],["sqrt(\u00bd)","SQRT1_2"],["\u221e","INFINITY"]]}],output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_CONSTANT_TOOLTIP}",helpUrl:"%{BKY_MATH_CONSTANT_HELPURL}"},{type:"math_number_property",
message0:"%1 %2",args0:[{type:"input_value",name:"NUMBER_TO_CHECK",check:"Number"},{type:"field_dropdown",name:"PROPERTY",options:[["%{BKY_MATH_IS_EVEN}","EVEN"],["%{BKY_MATH_IS_ODD}","ODD"],["%{BKY_MATH_IS_PRIME}","PRIME"],["%{BKY_MATH_IS_WHOLE}","WHOLE"],["%{BKY_MATH_IS_POSITIVE}","POSITIVE"],["%{BKY_MATH_IS_NEGATIVE}","NEGATIVE"],["%{BKY_MATH_IS_DIVISIBLE_BY}","DIVISIBLE_BY"]]}],inputsInline:!0,output:"Boolean",style:"math_blocks",tooltip:"%{BKY_MATH_IS_TOOLTIP}",mutator:"math_is_divisibleby_mutator"},
{type:"math_change",message0:"%{BKY_MATH_CHANGE_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:"%{BKY_MATH_CHANGE_TITLE_ITEM}"},{type:"input_value",name:"DELTA",check:"Number"}],previousStatement:null,nextStatement:null,style:"variable_blocks",helpUrl:"%{BKY_MATH_CHANGE_HELPURL}",extensions:["math_change_tooltip"]},{type:"math_round",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ROUND_OPERATOR_ROUND}","ROUND"],["%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}","ROUNDUP"],
["%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}","ROUNDDOWN"]]},{type:"input_value",name:"NUM",check:"Number"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ROUND_HELPURL}",tooltip:"%{BKY_MATH_ROUND_TOOLTIP}"},{type:"math_on_list",message0:"%1 %2",args0:[{type:"field_dropdown",name:"OP",options:[["%{BKY_MATH_ONLIST_OPERATOR_SUM}","SUM"],["%{BKY_MATH_ONLIST_OPERATOR_MIN}","MIN"],["%{BKY_MATH_ONLIST_OPERATOR_MAX}","MAX"],["%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}","AVERAGE"],["%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}",
"MEDIAN"],["%{BKY_MATH_ONLIST_OPERATOR_MODE}","MODE"],["%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}","STD_DEV"],["%{BKY_MATH_ONLIST_OPERATOR_RANDOM}","RANDOM"]]},{type:"input_value",name:"LIST",check:"Array"}],output:"Number",style:"math_blocks",helpUrl:"%{BKY_MATH_ONLIST_HELPURL}",mutator:"math_modes_of_list_mutator",extensions:["math_op_tooltip"]},{type:"math_modulo",message0:"%{BKY_MATH_MODULO_TITLE}",args0:[{type:"input_value",name:"DIVIDEND",check:"Number"},{type:"input_value",name:"DIVISOR",check:"Number"}],
inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_MODULO_TOOLTIP}",helpUrl:"%{BKY_MATH_MODULO_HELPURL}"},{type:"math_constrain",message0:"%{BKY_MATH_CONSTRAIN_TITLE}",args0:[{type:"input_value",name:"VALUE",check:"Number"},{type:"input_value",name:"LOW",check:"Number"},{type:"input_value",name:"HIGH",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_CONSTRAIN_TOOLTIP}",helpUrl:"%{BKY_MATH_CONSTRAIN_HELPURL}"},{type:"math_random_int",message0:"%{BKY_MATH_RANDOM_INT_TITLE}",
args0:[{type:"input_value",name:"FROM",check:"Number"},{type:"input_value",name:"TO",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_RANDOM_INT_TOOLTIP}",helpUrl:"%{BKY_MATH_RANDOM_INT_HELPURL}"},{type:"math_random_float",message0:"%{BKY_MATH_RANDOM_FLOAT_TITLE_RANDOM}",output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_RANDOM_FLOAT_TOOLTIP}",helpUrl:"%{BKY_MATH_RANDOM_FLOAT_HELPURL}"},{type:"math_atan2",message0:"%{BKY_MATH_ATAN2_TITLE}",args0:[{type:"input_value",
name:"X",check:"Number"},{type:"input_value",name:"Y",check:"Number"}],inputsInline:!0,output:"Number",style:"math_blocks",tooltip:"%{BKY_MATH_ATAN2_TOOLTIP}",helpUrl:"%{BKY_MATH_ATAN2_HELPURL}"}]),TOOLTIPS_BY_OP$$module$build$src$blocks$math={ADD:"%{BKY_MATH_ARITHMETIC_TOOLTIP_ADD}",MINUS:"%{BKY_MATH_ARITHMETIC_TOOLTIP_MINUS}",MULTIPLY:"%{BKY_MATH_ARITHMETIC_TOOLTIP_MULTIPLY}",DIVIDE:"%{BKY_MATH_ARITHMETIC_TOOLTIP_DIVIDE}",POWER:"%{BKY_MATH_ARITHMETIC_TOOLTIP_POWER}",ROOT:"%{BKY_MATH_SINGLE_TOOLTIP_ROOT}",
ABS:"%{BKY_MATH_SINGLE_TOOLTIP_ABS}",NEG:"%{BKY_MATH_SINGLE_TOOLTIP_NEG}",LN:"%{BKY_MATH_SINGLE_TOOLTIP_LN}",LOG10:"%{BKY_MATH_SINGLE_TOOLTIP_LOG10}",EXP:"%{BKY_MATH_SINGLE_TOOLTIP_EXP}",POW10:"%{BKY_MATH_SINGLE_TOOLTIP_POW10}",SIN:"%{BKY_MATH_TRIG_TOOLTIP_SIN}",COS:"%{BKY_MATH_TRIG_TOOLTIP_COS}",TAN:"%{BKY_MATH_TRIG_TOOLTIP_TAN}",ASIN:"%{BKY_MATH_TRIG_TOOLTIP_ASIN}",ACOS:"%{BKY_MATH_TRIG_TOOLTIP_ACOS}",ATAN:"%{BKY_MATH_TRIG_TOOLTIP_ATAN}",SUM:"%{BKY_MATH_ONLIST_TOOLTIP_SUM}",MIN:"%{BKY_MATH_ONLIST_TOOLTIP_MIN}",
MAX:"%{BKY_MATH_ONLIST_TOOLTIP_MAX}",AVERAGE:"%{BKY_MATH_ONLIST_TOOLTIP_AVERAGE}",MEDIAN:"%{BKY_MATH_ONLIST_TOOLTIP_MEDIAN}",MODE:"%{BKY_MATH_ONLIST_TOOLTIP_MODE}",STD_DEV:"%{BKY_MATH_ONLIST_TOOLTIP_STD_DEV}",RANDOM:"%{BKY_MATH_ONLIST_TOOLTIP_RANDOM}"};$.register$$module$build$src$core$extensions("math_op_tooltip",$.buildTooltipForDropdown$$module$build$src$core$extensions("OP",TOOLTIPS_BY_OP$$module$build$src$blocks$math));
var IS_DIVISIBLEBY_MUTATOR_MIXIN$$module$build$src$blocks$math={mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation"),b=this.getFieldValue("PROPERTY")==="DIVISIBLE_BY";a.setAttribute("divisor_input",String(b));return a},domToMutation:function(a){a=a.getAttribute("divisor_input")==="true";this.updateShape_(a)},updateShape_:function(a){const b=this.getInput("DIVISOR");a?b||this.appendValueInput("DIVISOR").setCheck("Number"):b&&this.removeInput("DIVISOR")}},IS_DIVISIBLE_MUTATOR_EXTENSION$$module$build$src$blocks$math=
function(){this.getField("PROPERTY").setValidator(function(a){a=a==="DIVISIBLE_BY";this.getSourceBlock().updateShape_(a)})};$.registerMutator$$module$build$src$core$extensions("math_is_divisibleby_mutator",IS_DIVISIBLEBY_MUTATOR_MIXIN$$module$build$src$blocks$math,IS_DIVISIBLE_MUTATOR_EXTENSION$$module$build$src$blocks$math);$.register$$module$build$src$core$extensions("math_change_tooltip",$.buildTooltipWithFieldText$$module$build$src$core$extensions("%{BKY_MATH_CHANGE_TOOLTIP}","VAR"));
var LIST_MODES_MUTATOR_MIXIN$$module$build$src$blocks$math={updateType_:function(a){a==="MODE"?this.outputConnection.setCheck("Array"):this.outputConnection.setCheck("Number")},mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");a.setAttribute("op",this.getFieldValue("OP"));return a},domToMutation:function(a){a=a.getAttribute("op");if(a===null)throw new TypeError("xmlElement had no op attribute");this.updateType_(a)}},LIST_MODES_MUTATOR_EXTENSION$$module$build$src$blocks$math=
function(){this.getField("OP").setValidator(function(a){this.updateType_(a)}.bind(this))};$.registerMutator$$module$build$src$core$extensions("math_modes_of_list_mutator",LIST_MODES_MUTATOR_MIXIN$$module$build$src$blocks$math,LIST_MODES_MUTATOR_EXTENSION$$module$build$src$blocks$math);$.defineBlocks$$module$build$src$core$common(blocks$$module$build$src$blocks$math);var module$build$src$blocks$math={blocks:blocks$$module$build$src$blocks$math};var blocks$$module$build$src$blocks$loops=$.createBlockDefinitionsFromJsonArray$$module$build$src$core$common([{type:"controls_repeat_ext",message0:"%{BKY_CONTROLS_REPEAT_TITLE}",args0:[{type:"input_value",name:"TIMES",check:"Number"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",tooltip:"%{BKY_CONTROLS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_CONTROLS_REPEAT_HELPURL}"},{type:"controls_repeat",message0:"%{BKY_CONTROLS_REPEAT_TITLE}",
args0:[{type:"field_number",name:"TIMES",value:10,min:0,precision:1}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",tooltip:"%{BKY_CONTROLS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_CONTROLS_REPEAT_HELPURL}"},{type:"controls_whileUntil",message0:"%1 %2",args0:[{type:"field_dropdown",name:"MODE",options:[["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}","WHILE"],["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}","UNTIL"]]},
{type:"input_value",name:"BOOL",check:"Boolean"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_WHILEUNTIL_HELPURL}",extensions:["controls_whileUntil_tooltip"]},{type:"controls_for",message0:"%{BKY_CONTROLS_FOR_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:null},{type:"input_value",name:"FROM",check:"Number",align:"RIGHT"},{type:"input_value",name:"TO",check:"Number",
align:"RIGHT"},{type:"input_value",name:"BY",check:"Number",align:"RIGHT"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],inputsInline:!0,previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FOR_HELPURL}",extensions:["contextMenu_newGetVariableBlock","controls_for_tooltip"]},{type:"controls_forEach",message0:"%{BKY_CONTROLS_FOREACH_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:null},{type:"input_value",name:"LIST",
check:"Array"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FOREACH_HELPURL}",extensions:["contextMenu_newGetVariableBlock","controls_forEach_tooltip"]},{type:"controls_flow_statements",message0:"%1",args0:[{type:"field_dropdown",name:"FLOW",options:[["%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}","BREAK"],["%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}","CONTINUE"]]}],
previousStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}",suppressPrefixSuffix:!0,extensions:["controls_flow_tooltip","controls_flow_in_loop_check"]}]),WHILE_UNTIL_TOOLTIPS$$module$build$src$blocks$loops={WHILE:"%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_WHILE}",UNTIL:"%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL}"};$.register$$module$build$src$core$extensions("controls_whileUntil_tooltip",$.buildTooltipForDropdown$$module$build$src$core$extensions("MODE",WHILE_UNTIL_TOOLTIPS$$module$build$src$blocks$loops));
var BREAK_CONTINUE_TOOLTIPS$$module$build$src$blocks$loops={BREAK:"%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK}",CONTINUE:"%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE}"};$.register$$module$build$src$core$extensions("controls_flow_tooltip",$.buildTooltipForDropdown$$module$build$src$core$extensions("FLOW",BREAK_CONTINUE_TOOLTIPS$$module$build$src$blocks$loops));
var CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN$$module$build$src$blocks$loops={customContextMenu:function(a){if(!this.isInFlyout){var b=this.getField("VAR"),c=b.getVariable().getName();this.isCollapsed()||c===null||(b={type:"variables_get",fields:{VAR:b.saveState(!0)}},a.push({enabled:!0,text:$.Msg$$module$build$src$core$msg.VARIABLES_SET_CREATE_GET.replace("%1",c),callback:$.callbackFactory$$module$build$src$core$contextmenu(this,b)}))}}};
$.registerMixin$$module$build$src$core$extensions("contextMenu_newGetVariableBlock",CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN$$module$build$src$blocks$loops);$.register$$module$build$src$core$extensions("controls_for_tooltip",$.buildTooltipWithFieldText$$module$build$src$core$extensions("%{BKY_CONTROLS_FOR_TOOLTIP}","VAR"));$.register$$module$build$src$core$extensions("controls_forEach_tooltip",$.buildTooltipWithFieldText$$module$build$src$core$extensions("%{BKY_CONTROLS_FOREACH_TOOLTIP}","VAR"));
var loopTypes$$module$build$src$blocks$loops=new Set(["controls_repeat","controls_repeat_ext","controls_forEach","controls_for","controls_whileUntil"]),CONTROL_FLOW_NOT_IN_LOOP_DISABLED_REASON$$module$build$src$blocks$loops="CONTROL_FLOW_NOT_IN_LOOP",CONTROL_FLOW_IN_LOOP_CHECK_MIXIN$$module$build$src$blocks$loops={getSurroundLoop:function(){let a=this;do{if(loopTypes$$module$build$src$blocks$loops.has(a.type))return a;a=a.getSurroundParent()}while(a);return null},onchange:function(a){const b=this.workspace;
if(b.isDragging&&!b.isDragging()&&(a.type===$.BLOCK_MOVE$$module$build$src$core$events$events||a.type===$.BLOCK_CREATE$$module$build$src$core$events$events)&&(a=!!this.getSurroundLoop(),this.setWarningText(a?null:$.Msg$$module$build$src$core$msg.CONTROLS_FLOW_STATEMENTS_WARNING),!this.isInFlyout))try{$.setRecordUndo$$module$build$src$core$events$utils(!1),this.setDisabledReason(!a,CONTROL_FLOW_NOT_IN_LOOP_DISABLED_REASON$$module$build$src$blocks$loops)}finally{$.setRecordUndo$$module$build$src$core$events$utils(!0)}}};
$.registerMixin$$module$build$src$core$extensions("controls_flow_in_loop_check",CONTROL_FLOW_IN_LOOP_CHECK_MIXIN$$module$build$src$blocks$loops);$.defineBlocks$$module$build$src$core$common(blocks$$module$build$src$blocks$loops);var module$build$src$blocks$loops={blocks:blocks$$module$build$src$blocks$loops,loopTypes:loopTypes$$module$build$src$blocks$loops};var blocks$$module$build$src$blocks$logic=$.createBlockDefinitionsFromJsonArray$$module$build$src$core$common([{type:"logic_boolean",message0:"%1",args0:[{type:"field_dropdown",name:"BOOL",options:[["%{BKY_LOGIC_BOOLEAN_TRUE}","TRUE"],["%{BKY_LOGIC_BOOLEAN_FALSE}","FALSE"]]}],output:"Boolean",style:"logic_blocks",tooltip:"%{BKY_LOGIC_BOOLEAN_TOOLTIP}",helpUrl:"%{BKY_LOGIC_BOOLEAN_HELPURL}"},{type:"controls_if",message0:"%{BKY_CONTROLS_IF_MSG_IF} %1",args0:[{type:"input_value",name:"IF0",check:"Boolean"}],
message1:"%{BKY_CONTROLS_IF_MSG_THEN} %1",args1:[{type:"input_statement",name:"DO0"}],previousStatement:null,nextStatement:null,style:"logic_blocks",helpUrl:"%{BKY_CONTROLS_IF_HELPURL}",suppressPrefixSuffix:!0,mutator:"controls_if_mutator",extensions:["controls_if_tooltip"]},{type:"controls_ifelse",message0:"%{BKY_CONTROLS_IF_MSG_IF} %1",args0:[{type:"input_value",name:"IF0",check:"Boolean"}],message1:"%{BKY_CONTROLS_IF_MSG_THEN} %1",args1:[{type:"input_statement",name:"DO0"}],message2:"%{BKY_CONTROLS_IF_MSG_ELSE} %1",
args2:[{type:"input_statement",name:"ELSE"}],previousStatement:null,nextStatement:null,style:"logic_blocks",tooltip:"%{BKYCONTROLS_IF_TOOLTIP_2}",helpUrl:"%{BKY_CONTROLS_IF_HELPURL}",suppressPrefixSuffix:!0,extensions:["controls_if_tooltip"]},{type:"logic_compare",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A"},{type:"field_dropdown",name:"OP",options:[["=","EQ"],["\u2260","NEQ"],["\u200f<","LT"],["\u200f\u2264","LTE"],["\u200f>","GT"],["\u200f\u2265","GTE"]]},{type:"input_value",name:"B"}],
inputsInline:!0,output:"Boolean",style:"logic_blocks",helpUrl:"%{BKY_LOGIC_COMPARE_HELPURL}",extensions:["logic_compare","logic_op_tooltip"]},{type:"logic_operation",message0:"%1 %2 %3",args0:[{type:"input_value",name:"A",check:"Boolean"},{type:"field_dropdown",name:"OP",options:[["%{BKY_LOGIC_OPERATION_AND}","AND"],["%{BKY_LOGIC_OPERATION_OR}","OR"]]},{type:"input_value",name:"B",check:"Boolean"}],inputsInline:!0,output:"Boolean",style:"logic_blocks",helpUrl:"%{BKY_LOGIC_OPERATION_HELPURL}",extensions:["logic_op_tooltip"]},
{type:"logic_negate",message0:"%{BKY_LOGIC_NEGATE_TITLE}",args0:[{type:"input_value",name:"BOOL",check:"Boolean"}],output:"Boolean",style:"logic_blocks",tooltip:"%{BKY_LOGIC_NEGATE_TOOLTIP}",helpUrl:"%{BKY_LOGIC_NEGATE_HELPURL}"},{type:"logic_null",message0:"%{BKY_LOGIC_NULL}",output:null,style:"logic_blocks",tooltip:"%{BKY_LOGIC_NULL_TOOLTIP}",helpUrl:"%{BKY_LOGIC_NULL_HELPURL}"},{type:"logic_ternary",message0:"%{BKY_LOGIC_TERNARY_CONDITION} %1",args0:[{type:"input_value",name:"IF",check:"Boolean"}],
message1:"%{BKY_LOGIC_TERNARY_IF_TRUE} %1",args1:[{type:"input_value",name:"THEN"}],message2:"%{BKY_LOGIC_TERNARY_IF_FALSE} %1",args2:[{type:"input_value",name:"ELSE"}],output:null,style:"logic_blocks",tooltip:"%{BKY_LOGIC_TERNARY_TOOLTIP}",helpUrl:"%{BKY_LOGIC_TERNARY_HELPURL}",extensions:["logic_ternary"]},{type:"controls_if_if",message0:"%{BKY_CONTROLS_IF_IF_TITLE_IF}",nextStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_IF_TOOLTIP}"},{type:"controls_if_elseif",
message0:"%{BKY_CONTROLS_IF_ELSEIF_TITLE_ELSEIF}",previousStatement:null,nextStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_ELSEIF_TOOLTIP}"},{type:"controls_if_else",message0:"%{BKY_CONTROLS_IF_ELSE_TITLE_ELSE}",previousStatement:null,enableContextMenu:!1,style:"logic_blocks",tooltip:"%{BKY_CONTROLS_IF_ELSE_TOOLTIP}"}]),TOOLTIPS_BY_OP$$module$build$src$blocks$logic={EQ:"%{BKY_LOGIC_COMPARE_TOOLTIP_EQ}",NEQ:"%{BKY_LOGIC_COMPARE_TOOLTIP_NEQ}",LT:"%{BKY_LOGIC_COMPARE_TOOLTIP_LT}",
LTE:"%{BKY_LOGIC_COMPARE_TOOLTIP_LTE}",GT:"%{BKY_LOGIC_COMPARE_TOOLTIP_GT}",GTE:"%{BKY_LOGIC_COMPARE_TOOLTIP_GTE}",AND:"%{BKY_LOGIC_OPERATION_TOOLTIP_AND}",OR:"%{BKY_LOGIC_OPERATION_TOOLTIP_OR}"};$.register$$module$build$src$core$extensions("logic_op_tooltip",$.buildTooltipForDropdown$$module$build$src$core$extensions("OP",TOOLTIPS_BY_OP$$module$build$src$blocks$logic));
var CONTROLS_IF_MUTATOR_MIXIN$$module$build$src$blocks$logic={elseifCount_:0,elseCount_:0,mutationToDom:function(){if(!this.elseifCount_&&!this.elseCount_)return null;const a=$.createElement$$module$build$src$core$utils$xml("mutation");this.elseifCount_&&a.setAttribute("elseif",String(this.elseifCount_));this.elseCount_&&a.setAttribute("else","1");return a},domToMutation:function(a){this.elseifCount_=parseInt(a.getAttribute("elseif"),10)||0;this.elseCount_=parseInt(a.getAttribute("else"),10)||0;this.rebuildShape_()},
saveExtraState:function(){if(!this.elseifCount_&&!this.elseCount_)return null;const a=Object.create(null);this.elseifCount_&&(a.elseIfCount=this.elseifCount_);this.elseCount_&&(a.hasElse=!0);return a},loadExtraState:function(a){this.elseifCount_=a.elseIfCount||0;this.elseCount_=a.hasElse?1:0;this.updateShape_()},decompose:function(a){const b=a.newBlock("controls_if_if");b.initSvg();let c=b.nextConnection;for(let d=1;d<=this.elseifCount_;d++){const e=a.newBlock("controls_if_elseif");e.initSvg();c.connect(e.previousConnection);
c=e.nextConnection}this.elseCount_&&(a=a.newBlock("controls_if_else"),a.initSvg(),c.connect(a.previousConnection));return b},compose:function(a){a=a.nextConnection.targetBlock();this.elseCount_=this.elseifCount_=0;const b=[null],c=[null];let d=null;for(;a;){if(!a.isInsertionMarker())switch(a.type){case "controls_if_elseif":this.elseifCount_++;b.push(a.valueConnection_);c.push(a.statementConnection_);break;case "controls_if_else":this.elseCount_++;d=a.statementConnection_;break;default:throw TypeError("Unknown block type: "+
a.type);}a=a.getNextBlock()}this.updateShape_();this.reconnectChildBlocks_(b,c,d)},saveConnections:function(a){a=a.nextConnection.targetBlock();let b=1;for(;a;){if(!a.isInsertionMarker())switch(a.type){case "controls_if_elseif":var c=this.getInput("IF"+b);const d=this.getInput("DO"+b);a.valueConnection_=c&&c.connection.targetConnection;a.statementConnection_=d&&d.connection.targetConnection;b++;break;case "controls_if_else":c=this.getInput("ELSE");a.statementConnection_=c&&c.connection.targetConnection;
break;default:throw TypeError("Unknown block type: "+a.type);}a=a.getNextBlock()}},rebuildShape_:function(){const a=[null],b=[null];let c=null;this.getInput("ELSE")&&(c=this.getInput("ELSE").connection.targetConnection);for(let d=1;this.getInput("IF"+d);d++){const e=this.getInput("IF"+d),f=this.getInput("DO"+d);a.push(e.connection.targetConnection);b.push(f.connection.targetConnection)}this.updateShape_();this.reconnectChildBlocks_(a,b,c)},updateShape_:function(){this.getInput("ELSE")&&this.removeInput("ELSE");
for(var a=1;this.getInput("IF"+a);a++)this.removeInput("IF"+a),this.removeInput("DO"+a);for(a=1;a<=this.elseifCount_;a++)this.appendValueInput("IF"+a).setCheck("Boolean").appendField($.Msg$$module$build$src$core$msg.CONTROLS_IF_MSG_ELSEIF),this.appendStatementInput("DO"+a).appendField($.Msg$$module$build$src$core$msg.CONTROLS_IF_MSG_THEN);this.elseCount_&&this.appendStatementInput("ELSE").appendField($.Msg$$module$build$src$core$msg.CONTROLS_IF_MSG_ELSE)},reconnectChildBlocks_:function(a,b,c){for(let d=
1;d<=this.elseifCount_;d++){let e;(e=a[d])==null||e.reconnect(this,"IF"+d);let f;(f=b[d])==null||f.reconnect(this,"DO"+d)}c==null||c.reconnect(this,"ELSE")}};$.registerMutator$$module$build$src$core$extensions("controls_if_mutator",CONTROLS_IF_MUTATOR_MIXIN$$module$build$src$blocks$logic,null,["controls_if_elseif","controls_if_else"]);
var CONTROLS_IF_TOOLTIP_EXTENSION$$module$build$src$blocks$logic=function(){this.setTooltip(function(){if(this.elseifCount_||this.elseCount_){if(!this.elseifCount_&&this.elseCount_)return $.Msg$$module$build$src$core$msg.CONTROLS_IF_TOOLTIP_2;if(this.elseifCount_&&!this.elseCount_)return $.Msg$$module$build$src$core$msg.CONTROLS_IF_TOOLTIP_3;if(this.elseifCount_&&this.elseCount_)return $.Msg$$module$build$src$core$msg.CONTROLS_IF_TOOLTIP_4}else return $.Msg$$module$build$src$core$msg.CONTROLS_IF_TOOLTIP_1;
return""}.bind(this))};$.register$$module$build$src$core$extensions("controls_if_tooltip",CONTROLS_IF_TOOLTIP_EXTENSION$$module$build$src$blocks$logic);
var LOGIC_COMPARE_ONCHANGE_MIXIN$$module$build$src$blocks$logic={onchange:function(a){this.prevBlocks_||(this.prevBlocks_=[null,null]);var b=this.getInputTargetBlock("A");const c=this.getInputTargetBlock("B");b&&c&&!this.workspace.connectionChecker.doTypeChecks(b.outputConnection,c.outputConnection)&&($.setGroup$$module$build$src$core$events$utils(a.group),a=this.prevBlocks_[0],a!==b&&(b.unplug(),!a||a.isDisposed()||a.isShadow()||this.getInput("A").connection.connect(a.outputConnection)),b=this.prevBlocks_[1],
b!==c&&(c.unplug(),!b||b.isDisposed()||b.isShadow()||this.getInput("B").connection.connect(b.outputConnection)),this.bumpNeighbours(),$.setGroup$$module$build$src$core$events$utils(!1));this.prevBlocks_[0]=this.getInputTargetBlock("A");this.prevBlocks_[1]=this.getInputTargetBlock("B")}},LOGIC_COMPARE_EXTENSION$$module$build$src$blocks$logic=function(){this.mixin(LOGIC_COMPARE_ONCHANGE_MIXIN$$module$build$src$blocks$logic)};$.register$$module$build$src$core$extensions("logic_compare",LOGIC_COMPARE_EXTENSION$$module$build$src$blocks$logic);
var LOGIC_TERNARY_ONCHANGE_MIXIN$$module$build$src$blocks$logic={prevParentConnection_:null,onchange:function(a){const b=this.getInputTargetBlock("THEN"),c=this.getInputTargetBlock("ELSE"),d=this.outputConnection.targetConnection;if((b||c)&&d)for(let e=0;e<2;e++){const f=e===1?b:c;f&&!f.workspace.connectionChecker.doTypeChecks(f.outputConnection,d)&&($.setGroup$$module$build$src$core$events$utils(a.group),d===this.prevParentConnection_?(this.unplug(),d.getSourceBlock().bumpNeighbours()):(f.unplug(),
f.bumpNeighbours()),$.setGroup$$module$build$src$core$events$utils(!1))}this.prevParentConnection_=d}};$.registerMixin$$module$build$src$core$extensions("logic_ternary",LOGIC_TERNARY_ONCHANGE_MIXIN$$module$build$src$blocks$logic);$.defineBlocks$$module$build$src$core$common(blocks$$module$build$src$blocks$logic);var module$build$src$blocks$logic={blocks:blocks$$module$build$src$blocks$logic};var blocks$$module$build$src$blocks$lists=$.createBlockDefinitionsFromJsonArray$$module$build$src$core$common([{type:"lists_create_empty",message0:"%{BKY_LISTS_CREATE_EMPTY_TITLE}",output:"Array",style:"list_blocks",tooltip:"%{BKY_LISTS_CREATE_EMPTY_TOOLTIP}",helpUrl:"%{BKY_LISTS_CREATE_EMPTY_HELPURL}"},{type:"lists_repeat",message0:"%{BKY_LISTS_REPEAT_TITLE}",args0:[{type:"input_value",name:"ITEM"},{type:"input_value",name:"NUM",check:"Number"}],output:"Array",style:"list_blocks",tooltip:"%{BKY_LISTS_REPEAT_TOOLTIP}",
helpUrl:"%{BKY_LISTS_REPEAT_HELPURL}"},{type:"lists_reverse",message0:"%{BKY_LISTS_REVERSE_MESSAGE0}",args0:[{type:"input_value",name:"LIST",check:"Array"}],output:"Array",inputsInline:!0,style:"list_blocks",tooltip:"%{BKY_LISTS_REVERSE_TOOLTIP}",helpUrl:"%{BKY_LISTS_REVERSE_HELPURL}"},{type:"lists_isEmpty",message0:"%{BKY_LISTS_ISEMPTY_TITLE}",args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Boolean",style:"list_blocks",tooltip:"%{BKY_LISTS_ISEMPTY_TOOLTIP}",helpUrl:"%{BKY_LISTS_ISEMPTY_HELPURL}"},
{type:"lists_length",message0:"%{BKY_LISTS_LENGTH_TITLE}",args0:[{type:"input_value",name:"VALUE",check:["String","Array"]}],output:"Number",style:"list_blocks",tooltip:"%{BKY_LISTS_LENGTH_TOOLTIP}",helpUrl:"%{BKY_LISTS_LENGTH_HELPURL}"}]),LISTS_CREATE_WITH$$module$build$src$blocks$lists={init:function(){this.setHelpUrl($.Msg$$module$build$src$core$msg.LISTS_CREATE_WITH_HELPURL);this.setStyle("list_blocks");this.itemCount_=3;this.updateShape_();this.setOutput(!0,"Array");this.setMutator(new $.MutatorIcon$$module$build$src$core$icons$mutator_icon(["lists_create_with_item"],
this));this.setTooltip($.Msg$$module$build$src$core$msg.LISTS_CREATE_WITH_TOOLTIP)},mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");a.setAttribute("items",String(this.itemCount_));return a},domToMutation:function(a){a=a.getAttribute("items");if(!a)throw new TypeError("element did not have items");this.itemCount_=parseInt(a,10);this.updateShape_()},saveExtraState:function(){return{itemCount:this.itemCount_}},loadExtraState:function(a){this.itemCount_=a.itemCount;
this.updateShape_()},decompose:function(a){const b=a.newBlock("lists_create_with_container");b.initSvg();let c=b.getInput("STACK").connection;for(let d=0;d<this.itemCount_;d++){const e=a.newBlock("lists_create_with_item");e.initSvg();if(!e.previousConnection)throw Error("itemBlock has no previousConnection");c.connect(e.previousConnection);c=e.nextConnection}return b},compose:function(a){var b=a.getInputTargetBlock("STACK");for(a=[];b;)b.isInsertionMarker()||a.push(b.valueConnection_),b=b.getNextBlock();
for(b=0;b<this.itemCount_;b++){const c=this.getInput("ADD"+b).connection.targetConnection;c&&!a.includes(c)&&c.disconnect()}this.itemCount_=a.length;this.updateShape_();for(b=0;b<this.itemCount_;b++){let c;(c=a[b])==null||c.reconnect(this,"ADD"+b)}},saveConnections:function(a){a=a.getInputTargetBlock("STACK");let b=0;for(;a;){if(a.isInsertionMarker()){a=a.getNextBlock();continue}const c=this.getInput("ADD"+b);let d;a.valueConnection_=(d=c)==null?void 0:d.connection.targetConnection;a=a.getNextBlock();
b++}},updateShape_:function(){this.itemCount_&&this.getInput("EMPTY")?this.removeInput("EMPTY"):this.itemCount_||this.getInput("EMPTY")||this.appendDummyInput("EMPTY").appendField($.Msg$$module$build$src$core$msg.LISTS_CREATE_EMPTY_TITLE);for(var a=0;a<this.itemCount_;a++)if(!this.getInput("ADD"+a)){const b=this.appendValueInput("ADD"+a).setAlign($.Align$$module$build$src$core$inputs$align.RIGHT);a===0&&b.appendField($.Msg$$module$build$src$core$msg.LISTS_CREATE_WITH_INPUT_WITH)}for(a=this.itemCount_;this.getInput("ADD"+
a);a++)this.removeInput("ADD"+a)}};blocks$$module$build$src$blocks$lists.lists_create_with=LISTS_CREATE_WITH$$module$build$src$blocks$lists;var LISTS_CREATE_WITH_CONTAINER$$module$build$src$blocks$lists={init:function(){this.setStyle("list_blocks");this.appendDummyInput().appendField($.Msg$$module$build$src$core$msg.LISTS_CREATE_WITH_CONTAINER_TITLE_ADD);this.appendStatementInput("STACK");this.setTooltip($.Msg$$module$build$src$core$msg.LISTS_CREATE_WITH_CONTAINER_TOOLTIP);this.contextMenu=!1}};
blocks$$module$build$src$blocks$lists.lists_create_with_container=LISTS_CREATE_WITH_CONTAINER$$module$build$src$blocks$lists;var LISTS_CREATE_WITH_ITEM$$module$build$src$blocks$lists={init:function(){this.setStyle("list_blocks");this.appendDummyInput().appendField($.Msg$$module$build$src$core$msg.LISTS_CREATE_WITH_ITEM_TITLE);this.setPreviousStatement(!0);this.setNextStatement(!0);this.setTooltip($.Msg$$module$build$src$core$msg.LISTS_CREATE_WITH_ITEM_TOOLTIP);this.contextMenu=!1}};
blocks$$module$build$src$blocks$lists.lists_create_with_item=LISTS_CREATE_WITH_ITEM$$module$build$src$blocks$lists;
var LISTS_INDEXOF$$module$build$src$blocks$lists={init:function(){var a=[[$.Msg$$module$build$src$core$msg.LISTS_INDEX_OF_FIRST,"FIRST"],[$.Msg$$module$build$src$core$msg.LISTS_INDEX_OF_LAST,"LAST"]];this.setHelpUrl($.Msg$$module$build$src$core$msg.LISTS_INDEX_OF_HELPURL);this.setStyle("list_blocks");this.setOutput(!0,"Number");this.appendValueInput("VALUE").setCheck("Array").appendField($.Msg$$module$build$src$core$msg.LISTS_INDEX_OF_INPUT_IN_LIST);a=$.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",
options:a});if(!a)throw Error("field_dropdown not found");this.appendValueInput("FIND").appendField(a,"END");this.setInputsInline(!0);this.setTooltip(()=>$.Msg$$module$build$src$core$msg.LISTS_INDEX_OF_TOOLTIP.replace("%1",this.workspace.options.oneBasedIndex?"0":"-1"))}};blocks$$module$build$src$blocks$lists.lists_indexOf=LISTS_INDEXOF$$module$build$src$blocks$lists;
var LISTS_GETINDEX$$module$build$src$blocks$lists={init:function(){var a=[[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_GET,"GET"],[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_GET_REMOVE,"GET_REMOVE"],[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_REMOVE,"REMOVE"]];this.WHERE_OPTIONS=[[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_FROM_START,"FROM_START"],[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_FROM_END,"FROM_END"],[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_FIRST,"FIRST"],
[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_LAST,"LAST"],[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_RANDOM,"RANDOM"]];this.setHelpUrl($.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_HELPURL);this.setStyle("list_blocks");a=$.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",options:a});a.setValidator(function(b){b=b==="REMOVE";this.getSourceBlock().updateStatement_(b)});this.appendValueInput("VALUE").setCheck("Array").appendField($.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_INPUT_IN_LIST);
this.appendDummyInput().appendField(a,"MODE").appendField("","SPACE");a=$.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",options:this.WHERE_OPTIONS});a.setValidator(function(b){const c=this.getValue();b=b==="FROM_START"||b==="FROM_END";b!==(c==="FROM_START"||c==="FROM_END")&&this.getSourceBlock().updateAt_(b)});this.appendDummyInput().appendField(a,"WHERE");this.appendDummyInput("AT");$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TAIL&&this.appendDummyInput("TAIL").appendField($.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TAIL);
this.setInputsInline(!0);this.setOutput(!0);this.updateAt_(!0);this.setTooltip(()=>{const b=this.getFieldValue("MODE"),c=this.getFieldValue("WHERE");let d="";switch(b+" "+c){case "GET FROM_START":case "GET FROM_END":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_GET_FROM;break;case "GET FIRST":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_GET_FIRST;break;case "GET LAST":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_GET_LAST;break;case "GET RANDOM":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_GET_RANDOM;
break;case "GET_REMOVE FROM_START":case "GET_REMOVE FROM_END":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM;break;case "GET_REMOVE FIRST":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST;break;case "GET_REMOVE LAST":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST;break;case "GET_REMOVE RANDOM":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_RANDOM;break;case "REMOVE FROM_START":case "REMOVE FROM_END":d=
$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM;break;case "REMOVE FIRST":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST;break;case "REMOVE LAST":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST;break;case "REMOVE RANDOM":d=$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM}if(c==="FROM_START"||c==="FROM_END")d+=" "+(c==="FROM_START"?$.Msg$$module$build$src$core$msg.LISTS_INDEX_FROM_START_TOOLTIP:$.Msg$$module$build$src$core$msg.LISTS_INDEX_FROM_END_TOOLTIP).replace("%1",
this.workspace.options.oneBasedIndex?"#1":"#0");return d})},mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");a.setAttribute("statement",String(!this.outputConnection));const b=this.getInput("AT")instanceof $.ValueInput$$module$build$src$core$inputs$value_input;a.setAttribute("at",String(b));return a},domToMutation:function(a){const b=a.getAttribute("statement")==="true";this.updateStatement_(b);a=a.getAttribute("at")!=="false";this.updateAt_(a)},saveExtraState:function(){return this.outputConnection?
null:{isStatement:!0}},loadExtraState:function(a){a.isStatement?this.updateStatement_(!0):typeof a==="string"&&this.domToMutation($.textToDom$$module$build$src$core$utils$xml(a))},updateStatement_:function(a){a!==!this.outputConnection&&(this.unplug(!0,!0),a?(this.setOutput(!1),this.setPreviousStatement(!0),this.setNextStatement(!0)):(this.setPreviousStatement(!1),this.setNextStatement(!1),this.setOutput(!0)))},updateAt_:function(a){this.removeInput("AT");this.removeInput("ORDINAL",!0);a?(this.appendValueInput("AT").setCheck("Number"),
$.Msg$$module$build$src$core$msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL").appendField($.Msg$$module$build$src$core$msg.ORDINAL_NUMBER_SUFFIX)):this.appendDummyInput("AT");$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_TAIL&&this.moveInputBefore("TAIL",null)}};blocks$$module$build$src$blocks$lists.lists_getIndex=LISTS_GETINDEX$$module$build$src$blocks$lists;
var LISTS_SETINDEX$$module$build$src$blocks$lists={init:function(){var a=[[$.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_SET,"SET"],[$.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_INSERT,"INSERT"]];this.WHERE_OPTIONS=[[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_FROM_START,"FROM_START"],[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_FROM_END,"FROM_END"],[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_FIRST,"FIRST"],[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_LAST,"LAST"],[$.Msg$$module$build$src$core$msg.LISTS_GET_INDEX_RANDOM,
"RANDOM"]];this.setHelpUrl($.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_HELPURL);this.setStyle("list_blocks");this.appendValueInput("LIST").setCheck("Array").appendField($.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_INPUT_IN_LIST);a=$.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",options:a});this.appendDummyInput().appendField(a,"MODE").appendField("","SPACE");a=$.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",options:this.WHERE_OPTIONS});a.setValidator(function(b){const c=
this.getValue();b=b==="FROM_START"||b==="FROM_END";b!==(c==="FROM_START"||c==="FROM_END")&&this.getSourceBlock().updateAt_(b)});this.appendDummyInput().appendField(a,"WHERE");this.appendDummyInput("AT");this.appendValueInput("TO").appendField($.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_INPUT_TO);this.setInputsInline(!0);this.setPreviousStatement(!0);this.setNextStatement(!0);this.setTooltip($.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_TOOLTIP);this.updateAt_(!0);this.setTooltip(()=>{const b=
this.getFieldValue("MODE"),c=this.getFieldValue("WHERE");let d="";switch(b+" "+c){case "SET FROM_START":case "SET FROM_END":d=$.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_TOOLTIP_SET_FROM;break;case "SET FIRST":d=$.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_TOOLTIP_SET_FIRST;break;case "SET LAST":d=$.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_TOOLTIP_SET_LAST;break;case "SET RANDOM":d=$.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_TOOLTIP_SET_RANDOM;break;case "INSERT FROM_START":case "INSERT FROM_END":d=
$.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FROM;break;case "INSERT FIRST":d=$.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST;break;case "INSERT LAST":d=$.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_TOOLTIP_INSERT_LAST;break;case "INSERT RANDOM":d=$.Msg$$module$build$src$core$msg.LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM}if(c==="FROM_START"||c==="FROM_END")d+=" "+$.Msg$$module$build$src$core$msg.LISTS_INDEX_FROM_START_TOOLTIP.replace("%1",this.workspace.options.oneBasedIndex?
"#1":"#0");return d})},mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation"),b=this.getInput("AT")instanceof $.ValueInput$$module$build$src$core$inputs$value_input;a.setAttribute("at",String(b));return a},domToMutation:function(a){a=a.getAttribute("at")!=="false";this.updateAt_(a)},saveExtraState:function(){return null},loadExtraState:function(){},updateAt_:function(a){this.removeInput("AT");this.removeInput("ORDINAL",!0);a?(this.appendValueInput("AT").setCheck("Number"),
$.Msg$$module$build$src$core$msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL").appendField($.Msg$$module$build$src$core$msg.ORDINAL_NUMBER_SUFFIX)):this.appendDummyInput("AT");this.moveInputBefore("AT","TO");this.getInput("ORDINAL")&&this.moveInputBefore("ORDINAL","TO")}};blocks$$module$build$src$blocks$lists.lists_setIndex=LISTS_SETINDEX$$module$build$src$blocks$lists;
var LISTS_GETSUBLIST$$module$build$src$blocks$lists={init:function(){this.WHERE_OPTIONS_1=[[$.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_START_FROM_START,"FROM_START"],[$.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_START_FROM_END,"FROM_END"],[$.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_START_FIRST,"FIRST"]];this.WHERE_OPTIONS_2=[[$.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_END_FROM_START,"FROM_START"],[$.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_END_FROM_END,"FROM_END"],
[$.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_END_LAST,"LAST"]];this.setHelpUrl($.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_HELPURL);this.setStyle("list_blocks");this.appendValueInput("LIST").setCheck("Array").appendField($.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_INPUT_IN_LIST);const a=b=>{const c=$.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",options:this["WHERE_OPTIONS_"+b]});c.setValidator(function(d){const e=this.getValue();d=d==="FROM_START"||d===
"FROM_END";d!==(e==="FROM_START"||e==="FROM_END")&&this.getSourceBlock().updateAt_(b,d)});return c};this.appendDummyInput("WHERE1_INPUT").appendField(a(1),"WHERE1");this.appendDummyInput("AT1");this.appendDummyInput("WHERE2_INPUT").appendField(a(2),"WHERE2");this.appendDummyInput("AT2");$.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_TAIL&&this.appendDummyInput("TAIL").appendField($.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_TAIL);this.setInputsInline(!0);this.setOutput(!0,"Array");this.updateAt_(1,
!0);this.updateAt_(2,!0);this.setTooltip($.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_TOOLTIP)},mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");var b=this.getInput("AT1")instanceof $.ValueInput$$module$build$src$core$inputs$value_input;a.setAttribute("at1",String(b));b=this.getInput("AT2")instanceof $.ValueInput$$module$build$src$core$inputs$value_input;a.setAttribute("at2",String(b));return a},domToMutation:function(a){const b=a.getAttribute("at1")===
"true";a=a.getAttribute("at2")==="true";this.updateAt_(1,b);this.updateAt_(2,a)},saveExtraState:function(){return null},loadExtraState:function(){},updateAt_:function(a,b){this.removeInput("AT"+a);this.removeInput("ORDINAL"+a,!0);b?(this.appendValueInput("AT"+a).setCheck("Number"),$.Msg$$module$build$src$core$msg.ORDINAL_NUMBER_SUFFIX&&this.appendDummyInput("ORDINAL"+a).appendField($.Msg$$module$build$src$core$msg.ORDINAL_NUMBER_SUFFIX)):this.appendDummyInput("AT"+a);a===1&&(this.moveInputBefore("AT1",
"WHERE2_INPUT"),this.getInput("ORDINAL1")&&this.moveInputBefore("ORDINAL1","WHERE2_INPUT"));$.Msg$$module$build$src$core$msg.LISTS_GET_SUBLIST_TAIL&&this.moveInputBefore("TAIL",null)}};blocks$$module$build$src$blocks$lists.lists_getSublist=LISTS_GETSUBLIST$$module$build$src$blocks$lists;
blocks$$module$build$src$blocks$lists.lists_sort={init:function(){this.jsonInit({message0:"%{BKY_LISTS_SORT_TITLE}",args0:[{type:"field_dropdown",name:"TYPE",options:[["%{BKY_LISTS_SORT_TYPE_NUMERIC}","NUMERIC"],["%{BKY_LISTS_SORT_TYPE_TEXT}","TEXT"],["%{BKY_LISTS_SORT_TYPE_IGNORECASE}","IGNORE_CASE"]]},{type:"field_dropdown",name:"DIRECTION",options:[["%{BKY_LISTS_SORT_ORDER_ASCENDING}","1"],["%{BKY_LISTS_SORT_ORDER_DESCENDING}","-1"]]},{type:"input_value",name:"LIST",check:"Array"}],output:"Array",
style:"list_blocks",tooltip:"%{BKY_LISTS_SORT_TOOLTIP}",helpUrl:"%{BKY_LISTS_SORT_HELPURL}"})}};
blocks$$module$build$src$blocks$lists.lists_split={init:function(){const a=$.fromJson$$module$build$src$core$field_registry({type:"field_dropdown",options:[[$.Msg$$module$build$src$core$msg.LISTS_SPLIT_LIST_FROM_TEXT,"SPLIT"],[$.Msg$$module$build$src$core$msg.LISTS_SPLIT_TEXT_FROM_LIST,"JOIN"]]});if(!a)throw Error("field_dropdown not found");a.setValidator(b=>{this.updateType_(b)});this.setHelpUrl($.Msg$$module$build$src$core$msg.LISTS_SPLIT_HELPURL);this.setStyle("list_blocks");this.appendValueInput("INPUT").setCheck("String").appendField(a,
"MODE");this.appendValueInput("DELIM").setCheck("String").appendField($.Msg$$module$build$src$core$msg.LISTS_SPLIT_WITH_DELIMITER);this.setInputsInline(!0);this.setOutput(!0,"Array");this.setTooltip(()=>{const b=this.getFieldValue("MODE");if(b==="SPLIT")return $.Msg$$module$build$src$core$msg.LISTS_SPLIT_TOOLTIP_SPLIT;if(b==="JOIN")return $.Msg$$module$build$src$core$msg.LISTS_SPLIT_TOOLTIP_JOIN;throw Error("Unknown mode: "+b);})},updateType_:function(a){if(this.getFieldValue("MODE")!==a){const b=
this.getInput("INPUT").connection;b.setShadowDom(null);const c=b.targetBlock();c&&(b.disconnect(),c.isShadow()?c.dispose(!1):this.bumpNeighbours())}a==="SPLIT"?(this.outputConnection.setCheck("Array"),this.getInput("INPUT").setCheck("String")):(this.outputConnection.setCheck("String"),this.getInput("INPUT").setCheck("Array"))},mutationToDom:function(){const a=$.createElement$$module$build$src$core$utils$xml("mutation");a.setAttribute("mode",this.getFieldValue("MODE"));return a},domToMutation:function(a){this.updateType_(a.getAttribute("mode"))},
saveExtraState:function(){return{mode:this.getFieldValue("MODE")}},loadExtraState:function(a){this.updateType_(a.mode)}};$.defineBlocks$$module$build$src$core$common(blocks$$module$build$src$blocks$lists);var module$build$src$blocks$lists={blocks:blocks$$module$build$src$blocks$lists};var blocks$$module$build$src$blocks$blocks=Object.assign({},blocks$$module$build$src$blocks$lists,blocks$$module$build$src$blocks$logic,blocks$$module$build$src$blocks$loops,blocks$$module$build$src$blocks$math,blocks$$module$build$src$blocks$procedures,blocks$$module$build$src$blocks$text,blocks$$module$build$src$blocks$variables,blocks$$module$build$src$blocks$variables_dynamic),module$build$src$blocks$blocks={blocks:blocks$$module$build$src$blocks$blocks,lists:module$build$src$blocks$lists,logic:module$build$src$blocks$logic,
loops:module$build$src$blocks$loops,math:module$build$src$blocks$math,procedures:module$build$src$blocks$procedures,texts:module$build$src$blocks$text,variables:module$build$src$blocks$variables,variablesDynamic:module$build$src$blocks$variables_dynamic};
module$build$src$blocks$blocks.__namespace__=$;
return module$build$src$blocks$blocks;
}));
//# sourceMappingURL=blocks_compressed.js.map

View File

@@ -0,0 +1,465 @@
/* eslint-disable */
;(function(root, factory) {
if (typeof define === 'function' && define.amd) { // AMD
define([], factory);
} else if (typeof exports === 'object') { // Node.js
module.exports = factory();
} else { // Browser
var messages = factory();
for (var key in messages) {
root.Blockly.Msg[key] = messages[key];
}
}
}(this, function() {
// This file was automatically generated. Do not modify.
'use strict';
var Blockly = Blockly || { Msg: Object.create(null) };
Blockly.Msg["ADD_COMMENT"] = "Kommentar hinzufügen";
Blockly.Msg["ALT_KEY"] = "Alt"; // untranslated
Blockly.Msg["CANNOT_DELETE_VARIABLE_PROCEDURE"] = "Die Variable „%1“ kann nicht gelöscht werden, da sie Teil der Definition der Funktion „%2“ ist.";
Blockly.Msg["CHANGE_VALUE_TITLE"] = "Wert ändern:";
Blockly.Msg["CHROME_OS"] = "ChromeOS"; // untranslated
Blockly.Msg["CLEAN_UP"] = "Bausteine aufräumen";
Blockly.Msg["CLOSE"] = "Close"; // untranslated
Blockly.Msg["COLLAPSED_WARNINGS_WARNING"] = "Eingeklappte Blöcke enthalten Warnungen.";
Blockly.Msg["COLLAPSE_ALL"] = "Alle Bausteine zusammenfalten";
Blockly.Msg["COLLAPSE_BLOCK"] = "Baustein zusammenfalten";
Blockly.Msg["COLOUR_BLEND_COLOUR1"] = "Farbe 1";
Blockly.Msg["COLOUR_BLEND_COLOUR2"] = "und Farbe 2";
Blockly.Msg["COLOUR_BLEND_HELPURL"] = "https://meyerweb.com/eric/tools/color-blend/#:::rgbp"; // untranslated
Blockly.Msg["COLOUR_BLEND_RATIO"] = "im Verhältnis";
Blockly.Msg["COLOUR_BLEND_TITLE"] = "mische";
Blockly.Msg["COLOUR_BLEND_TOOLTIP"] = "Vermischt 2 Farben mit konfigurierbarem Farbverhältnis (0.0 - 1.0).";
Blockly.Msg["COLOUR_PICKER_HELPURL"] = "https://de.wikipedia.org/wiki/Farbe";
Blockly.Msg["COLOUR_PICKER_TOOLTIP"] = "Wähle eine Farbe aus der Palette.";
Blockly.Msg["COLOUR_RANDOM_HELPURL"] = "http://randomcolour.com"; // untranslated
Blockly.Msg["COLOUR_RANDOM_TITLE"] = "zufällige Farbe";
Blockly.Msg["COLOUR_RANDOM_TOOLTIP"] = "Erzeugt eine Farbe nach dem Zufallsprinzip.";
Blockly.Msg["COLOUR_RGB_BLUE"] = "blau";
Blockly.Msg["COLOUR_RGB_GREEN"] = "grün";
Blockly.Msg["COLOUR_RGB_HELPURL"] = "https://www.december.com/html/spec/colorpercompact.html"; // untranslated
Blockly.Msg["COLOUR_RGB_RED"] = "rot";
Blockly.Msg["COLOUR_RGB_TITLE"] = "Farbe aus";
Blockly.Msg["COLOUR_RGB_TOOLTIP"] = "Erzeugt eine Farbe mit selbst definierten Rot-, Grün- und Blauwerten. Alle Werte müssen zwischen 0 und 100 liegen.";
Blockly.Msg["COMMAND_KEY"] = "⌘ Command"; // untranslated
Blockly.Msg["CONTROLS_FLOW_STATEMENTS_HELPURL"] = "https://de.wikipedia.org/wiki/Kontrollstruktur";
Blockly.Msg["CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK"] = "die Schleife abbrechen";
Blockly.Msg["CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE"] = "sofort mit nächstem Schleifendurchlauf fortfahren";
Blockly.Msg["CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK"] = "Die umgebende Schleife beenden.";
Blockly.Msg["CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE"] = "Diese Anweisung abbrechen und mit dem nächsten Schleifendurchlauf fortfahren.";
Blockly.Msg["CONTROLS_FLOW_STATEMENTS_WARNING"] = "Warnung: Dieser Baustein kann nur in einer Schleife verwendet werden.";
Blockly.Msg["CONTROLS_FOREACH_HELPURL"] = "https://de.wikipedia.org/wiki/For-Schleife";
Blockly.Msg["CONTROLS_FOREACH_TITLE"] = "für jeden Wert %1 aus der Liste %2";
Blockly.Msg["CONTROLS_FOREACH_TOOLTIP"] = "Führt eine Anweisung für jeden Wert in der Liste aus und setzt dabei die Variable \"%1\" auf den aktuellen Listenwert.";
Blockly.Msg["CONTROLS_FOR_HELPURL"] = "https://de.wikipedia.org/wiki/For-Schleife";
Blockly.Msg["CONTROLS_FOR_TITLE"] = "zähle %1 von %2 bis %3 in Schritten von %4";
Blockly.Msg["CONTROLS_FOR_TOOLTIP"] = "Zählt die Variable \"%1\" von einem Startwert bis zu einem Endwert und führt für jeden Wert eine Anweisung aus.";
Blockly.Msg["CONTROLS_IF_ELSEIF_TOOLTIP"] = "Eine weitere Bedingung hinzufügen.";
Blockly.Msg["CONTROLS_IF_ELSE_TOOLTIP"] = "Eine sonst-Bedingung hinzufügen. Führt eine Anweisung aus, falls keine Bedingung zutrifft.";
Blockly.Msg["CONTROLS_IF_HELPURL"] = "https://github.com/google/blockly/wiki/IfElse"; // untranslated
Blockly.Msg["CONTROLS_IF_IF_TOOLTIP"] = "Hinzufügen, entfernen oder sortieren von Sektionen";
Blockly.Msg["CONTROLS_IF_MSG_ELSE"] = "sonst";
Blockly.Msg["CONTROLS_IF_MSG_ELSEIF"] = "sonst falls";
Blockly.Msg["CONTROLS_IF_MSG_IF"] = "falls";
Blockly.Msg["CONTROLS_IF_TOOLTIP_1"] = "Führt eine Anweisung aus, falls eine Bedingung wahr ist.";
Blockly.Msg["CONTROLS_IF_TOOLTIP_2"] = "Führt die erste Anweisung aus, falls eine Bedingung wahr ist. Führt ansonsten die zweite Anweisung aus.";
Blockly.Msg["CONTROLS_IF_TOOLTIP_3"] = "Führt die erste Anweisung aus, falls die erste Bedingung wahr ist. Führt ansonsten die zweite Anweisung aus, falls die zweite Bedingung wahr ist.";
Blockly.Msg["CONTROLS_IF_TOOLTIP_4"] = "Führe die erste Anweisung aus, falls die erste Bedingung wahr ist. Führt ansonsten die zweite Anweisung aus, falls die zweite Bedingung wahr ist. Führt die dritte Anweisung aus, falls keine der beiden Bedingungen wahr ist.";
Blockly.Msg["CONTROLS_REPEAT_HELPURL"] = "https://de.wikipedia.org/wiki/For-Schleife";
Blockly.Msg["CONTROLS_REPEAT_INPUT_DO"] = "mache";
Blockly.Msg["CONTROLS_REPEAT_TITLE"] = "wiederhole %1-mal:";
Blockly.Msg["CONTROLS_REPEAT_TOOLTIP"] = "Eine Anweisung mehrfach ausführen.";
Blockly.Msg["CONTROLS_WHILEUNTIL_HELPURL"] = "https://de.wikipedia.org/wiki/Schleife_(Programmierung)#Kopfgesteuerte_Schleife";
Blockly.Msg["CONTROLS_WHILEUNTIL_OPERATOR_UNTIL"] = "wiederhole bis";
Blockly.Msg["CONTROLS_WHILEUNTIL_OPERATOR_WHILE"] = "wiederhole solange";
Blockly.Msg["CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL"] = "Führt Anweisungen aus, solange die Bedingung unwahr ist.";
Blockly.Msg["CONTROLS_WHILEUNTIL_TOOLTIP_WHILE"] = "Führt Anweisungen aus, solange die Bedingung wahr ist.";
Blockly.Msg["CONTROL_KEY"] = "Ctrl"; // untranslated
Blockly.Msg["COPY_SHORTCUT"] = "Copy"; // untranslated
Blockly.Msg["CUT_SHORTCUT"] = "Cut"; // untranslated
Blockly.Msg["DELETE_ALL_BLOCKS"] = "Alle %1 Bausteine löschen?";
Blockly.Msg["DELETE_BLOCK"] = "Baustein löschen";
Blockly.Msg["DELETE_VARIABLE"] = "Die Variable „%1“ löschen";
Blockly.Msg["DELETE_VARIABLE_CONFIRMATION"] = "%1 Verwendungen der Variable „%2“ löschen?";
Blockly.Msg["DELETE_X_BLOCKS"] = "%1 Bausteine löschen";
Blockly.Msg["DIALOG_CANCEL"] = "Abbrechen";
Blockly.Msg["DIALOG_OK"] = "Okay";
Blockly.Msg["DISABLE_BLOCK"] = "Baustein deaktivieren";
Blockly.Msg["DUPLICATE_BLOCK"] = "Kopieren";
Blockly.Msg["DUPLICATE_COMMENT"] = "Kommentar duplizieren";
Blockly.Msg["EDIT_BLOCK_CONTENTS"] = "Edit Block contents"; // untranslated
Blockly.Msg["ENABLE_BLOCK"] = "Baustein aktivieren";
Blockly.Msg["EXPAND_ALL"] = "Alle Bausteine entfalten";
Blockly.Msg["EXPAND_BLOCK"] = "Baustein entfalten";
Blockly.Msg["EXTERNAL_INPUTS"] = "externe Eingänge";
Blockly.Msg["HELP"] = "Hilfe";
Blockly.Msg["HELP_PROMPT"] = "Press %1 for help on keyboard controls"; // untranslated
Blockly.Msg["INLINE_INPUTS"] = "interne Eingänge";
Blockly.Msg["KEYBOARD_NAV_CONSTRAINED_MOVE_HINT"] = "Use the arrow keys to move, then %1 to accept the position"; // untranslated
Blockly.Msg["KEYBOARD_NAV_COPIED_HINT"] = "Copied. Press %1 to paste."; // untranslated
Blockly.Msg["KEYBOARD_NAV_CUT_HINT"] = "Cut. Press %1 to paste."; // untranslated
Blockly.Msg["KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT"] = "Hold %1 and use arrow keys to move freely, then %2 to accept the position"; // untranslated
Blockly.Msg["LINUX"] = "Linux"; // untranslated
Blockly.Msg["LISTS_CREATE_EMPTY_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#create-empty-list"; // untranslated
Blockly.Msg["LISTS_CREATE_EMPTY_TITLE"] = "erzeuge eine leere Liste";
Blockly.Msg["LISTS_CREATE_EMPTY_TOOLTIP"] = "Erzeugt eine leere Liste ohne Inhalt.";
Blockly.Msg["LISTS_CREATE_WITH_CONTAINER_TITLE_ADD"] = "Liste";
Blockly.Msg["LISTS_CREATE_WITH_CONTAINER_TOOLTIP"] = "Hinzufügen, entfernen und sortieren von Elementen.";
Blockly.Msg["LISTS_CREATE_WITH_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#create-list-with"; // untranslated
Blockly.Msg["LISTS_CREATE_WITH_INPUT_WITH"] = "erzeuge Liste mit";
Blockly.Msg["LISTS_CREATE_WITH_ITEM_TOOLTIP"] = "Ein Element zur Liste hinzufügen.";
Blockly.Msg["LISTS_CREATE_WITH_TOOLTIP"] = "Erzeugt eine Liste aus den angegebenen Elementen.";
Blockly.Msg["LISTS_GET_INDEX_FIRST"] = "das erste";
Blockly.Msg["LISTS_GET_INDEX_FROM_END"] = "von hinten das";
Blockly.Msg["LISTS_GET_INDEX_FROM_START"] = "das";
Blockly.Msg["LISTS_GET_INDEX_GET"] = "nimm";
Blockly.Msg["LISTS_GET_INDEX_GET_REMOVE"] = "nimm und entferne";
Blockly.Msg["LISTS_GET_INDEX_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list"; // untranslated
Blockly.Msg["LISTS_GET_INDEX_LAST"] = "das letzte";
Blockly.Msg["LISTS_GET_INDEX_RANDOM"] = "ein zufälliges";
Blockly.Msg["LISTS_GET_INDEX_REMOVE"] = "entferne";
Blockly.Msg["LISTS_GET_INDEX_TAIL"] = "Element";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_GET_FIRST"] = "Extrahiert das erste Element aus der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_GET_FROM"] = "Extrahiert das Element an der angegebenen Position in der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_GET_LAST"] = "Extrahiert das letzte Element aus der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_GET_RANDOM"] = "Extrahiert ein zufälliges Element aus der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST"] = "Extrahiert und entfernt das erste Element aus der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM"] = "Extrahiert und entfernt das Element an der angegebenen Position aus der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST"] = "Extrahiert und entfernt das letzte Element aus der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_RANDOM"] = "Extrahiert und entfernt ein zufälliges Element aus der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST"] = "Entfernt das erste Element aus der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM"] = "Entfernt das Element an der angegebenen Position aus der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST"] = "Entfernt das letzte Element aus der Liste.";
Blockly.Msg["LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM"] = "Entfernt ein zufälliges Element aus der Liste.";
Blockly.Msg["LISTS_GET_SUBLIST_END_FROM_END"] = "bis von hinten";
Blockly.Msg["LISTS_GET_SUBLIST_END_FROM_START"] = "bis";
Blockly.Msg["LISTS_GET_SUBLIST_END_LAST"] = "bis letztes";
Blockly.Msg["LISTS_GET_SUBLIST_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#getting-a-sublist"; // untranslated
Blockly.Msg["LISTS_GET_SUBLIST_START_FIRST"] = "nimm Teilliste ab erstes";
Blockly.Msg["LISTS_GET_SUBLIST_START_FROM_END"] = "nimm Teilliste ab von hinten";
Blockly.Msg["LISTS_GET_SUBLIST_START_FROM_START"] = "nimm Teilliste ab";
Blockly.Msg["LISTS_GET_SUBLIST_TAIL"] = "Element";
Blockly.Msg["LISTS_GET_SUBLIST_TOOLTIP"] = "Erstellt eine Kopie mit dem angegebenen Abschnitt der Liste.";
Blockly.Msg["LISTS_INDEX_FROM_END_TOOLTIP"] = "%1 ist das letzte Element.";
Blockly.Msg["LISTS_INDEX_FROM_START_TOOLTIP"] = "%1 ist das erste Element.";
Blockly.Msg["LISTS_INDEX_OF_FIRST"] = "suche erstes Auftreten von";
Blockly.Msg["LISTS_INDEX_OF_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list"; // untranslated
Blockly.Msg["LISTS_INDEX_OF_LAST"] = "suche letztes Auftreten von";
Blockly.Msg["LISTS_INDEX_OF_TOOLTIP"] = "Sucht die Position (Index) eines Elementes in der Liste. Gibt %1 zurück, falls kein Element gefunden wurde.";
Blockly.Msg["LISTS_INLIST"] = "in der Liste";
Blockly.Msg["LISTS_ISEMPTY_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#is-empty"; // untranslated
Blockly.Msg["LISTS_ISEMPTY_TITLE"] = "%1 ist leer";
Blockly.Msg["LISTS_ISEMPTY_TOOLTIP"] = "Ist wahr, falls die Liste leer ist.";
Blockly.Msg["LISTS_LENGTH_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#length-of"; // untranslated
Blockly.Msg["LISTS_LENGTH_TITLE"] = "Länge von %1";
Blockly.Msg["LISTS_LENGTH_TOOLTIP"] = "Die Anzahl von Elementen in der Liste.";
Blockly.Msg["LISTS_REPEAT_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#create-list-with"; // untranslated
Blockly.Msg["LISTS_REPEAT_TITLE"] = "erzeuge Liste mit %2-mal dem Element %1";
Blockly.Msg["LISTS_REPEAT_TOOLTIP"] = "Erzeugt eine Liste mit der angegebenen Anzahl an Elementen des angegebenen Wertes.";
Blockly.Msg["LISTS_REVERSE_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#reversing-a-list"; // untranslated
Blockly.Msg["LISTS_REVERSE_MESSAGE0"] = "kehre %1 um";
Blockly.Msg["LISTS_REVERSE_TOOLTIP"] = "Kehre eine Kopie einer Liste um.";
Blockly.Msg["LISTS_SET_INDEX_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#in-list--set"; // untranslated
Blockly.Msg["LISTS_SET_INDEX_INPUT_TO"] = "ein";
Blockly.Msg["LISTS_SET_INDEX_INSERT"] = "füge als";
Blockly.Msg["LISTS_SET_INDEX_SET"] = "setze für";
Blockly.Msg["LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST"] = "Fügt das Element an den Anfang der Liste an.";
Blockly.Msg["LISTS_SET_INDEX_TOOLTIP_INSERT_FROM"] = "Fügt das Element an der angegebenen Position in die Liste ein.";
Blockly.Msg["LISTS_SET_INDEX_TOOLTIP_INSERT_LAST"] = "Fügt das Element an das Ende der Liste an.";
Blockly.Msg["LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM"] = "Fügt das Element zufällig in die Liste ein.";
Blockly.Msg["LISTS_SET_INDEX_TOOLTIP_SET_FIRST"] = "Setzt das erste Element in der Liste.";
Blockly.Msg["LISTS_SET_INDEX_TOOLTIP_SET_FROM"] = "Setzt das Element an der angegebenen Position in der Liste.";
Blockly.Msg["LISTS_SET_INDEX_TOOLTIP_SET_LAST"] = "Setzt das letzte Element in die Liste.";
Blockly.Msg["LISTS_SET_INDEX_TOOLTIP_SET_RANDOM"] = "Setzt ein zufälliges Element in der Liste.";
Blockly.Msg["LISTS_SORT_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#sorting-a-list"; // untranslated
Blockly.Msg["LISTS_SORT_ORDER_ASCENDING"] = "aufsteigend";
Blockly.Msg["LISTS_SORT_ORDER_DESCENDING"] = "absteigend";
Blockly.Msg["LISTS_SORT_TITLE"] = "%1 %2 %3 sortieren";
Blockly.Msg["LISTS_SORT_TOOLTIP"] = "Eine Kopie einer Liste sortieren.";
Blockly.Msg["LISTS_SORT_TYPE_IGNORECASE"] = "alphabetisch, Großschreibung ignorieren";
Blockly.Msg["LISTS_SORT_TYPE_NUMERIC"] = "numerisch";
Blockly.Msg["LISTS_SORT_TYPE_TEXT"] = "alphabetisch";
Blockly.Msg["LISTS_SPLIT_HELPURL"] = "https://github.com/google/blockly/wiki/Lists#splitting-strings-and-joining-lists"; // untranslated
Blockly.Msg["LISTS_SPLIT_LIST_FROM_TEXT"] = "Liste aus Text erstellen";
Blockly.Msg["LISTS_SPLIT_TEXT_FROM_LIST"] = "Text aus Liste erstellen";
Blockly.Msg["LISTS_SPLIT_TOOLTIP_JOIN"] = "Liste mit Texten in einen Text vereinen, getrennt durch ein Trennzeichen.";
Blockly.Msg["LISTS_SPLIT_TOOLTIP_SPLIT"] = "Text in eine Liste mit Texten aufteilen, unterbrochen bei jedem Trennzeichen.";
Blockly.Msg["LISTS_SPLIT_WITH_DELIMITER"] = "mit Trennzeichen";
Blockly.Msg["LOGIC_BOOLEAN_FALSE"] = "falsch";
Blockly.Msg["LOGIC_BOOLEAN_HELPURL"] = "https://github.com/google/blockly/wiki/Logic#values"; // untranslated
Blockly.Msg["LOGIC_BOOLEAN_TOOLTIP"] = "Ist entweder wahr oder falsch.";
Blockly.Msg["LOGIC_BOOLEAN_TRUE"] = "wahr";
Blockly.Msg["LOGIC_COMPARE_HELPURL"] = "https://de.wikipedia.org/wiki/Vergleich_(Zahlen)";
Blockly.Msg["LOGIC_COMPARE_TOOLTIP_EQ"] = "Ist wahr, falls beide Werte gleich sind.";
Blockly.Msg["LOGIC_COMPARE_TOOLTIP_GT"] = "Ist wahr, falls der erste Wert größer als der zweite Wert ist.";
Blockly.Msg["LOGIC_COMPARE_TOOLTIP_GTE"] = "Ist wahr, falls der erste Wert größer als oder gleich groß wie der zweite Wert ist.";
Blockly.Msg["LOGIC_COMPARE_TOOLTIP_LT"] = "Ist wahr, falls der erste Wert kleiner als der zweite Wert ist.";
Blockly.Msg["LOGIC_COMPARE_TOOLTIP_LTE"] = "Ist wahr, falls der erste Wert kleiner als oder gleich groß wie der zweite Wert ist.";
Blockly.Msg["LOGIC_COMPARE_TOOLTIP_NEQ"] = "Ist wahr, falls beide Werte unterschiedlich sind.";
Blockly.Msg["LOGIC_NEGATE_HELPURL"] = "https://github.com/google/blockly/wiki/Logic#not"; // untranslated
Blockly.Msg["LOGIC_NEGATE_TITLE"] = "nicht %1";
Blockly.Msg["LOGIC_NEGATE_TOOLTIP"] = "Ist wahr, falls der Eingabewert unwahr ist. Ist unwahr, falls der Eingabewert wahr ist.";
Blockly.Msg["LOGIC_NULL"] = "null";
Blockly.Msg["LOGIC_NULL_HELPURL"] = "https://de.wikipedia.org/wiki/Nullwert";
Blockly.Msg["LOGIC_NULL_TOOLTIP"] = "Ist \"null\".";
Blockly.Msg["LOGIC_OPERATION_AND"] = "und";
Blockly.Msg["LOGIC_OPERATION_HELPURL"] = "https://github.com/google/blockly/wiki/Logic#logical-operations"; // untranslated
Blockly.Msg["LOGIC_OPERATION_OR"] = "oder";
Blockly.Msg["LOGIC_OPERATION_TOOLTIP_AND"] = "Ist wahr, falls beide Werte wahr sind.";
Blockly.Msg["LOGIC_OPERATION_TOOLTIP_OR"] = "Gibt true zurück, wenn mindestens eine der Eingaben wahr ist.";
Blockly.Msg["LOGIC_TERNARY_CONDITION"] = "prüfe";
Blockly.Msg["LOGIC_TERNARY_HELPURL"] = "https://de.wikipedia.org/wiki/%3F:#Auswahloperator";
Blockly.Msg["LOGIC_TERNARY_IF_FALSE"] = "falls falsch";
Blockly.Msg["LOGIC_TERNARY_IF_TRUE"] = "falls wahr";
Blockly.Msg["LOGIC_TERNARY_TOOLTIP"] = "Überprüft eine Bedingung \"prüfe\". Falls die Bedingung wahr ist, wird der \"falls wahr\"-Wert zurückgegeben, andernfalls der \"falls unwahr\"-Wert";
Blockly.Msg["MAC_OS"] = "macOS"; // untranslated
Blockly.Msg["MATH_ADDITION_SYMBOL"] = "+"; // untranslated
Blockly.Msg["MATH_ARITHMETIC_HELPURL"] = "https://de.wikipedia.org/wiki/Grundrechenart";
Blockly.Msg["MATH_ARITHMETIC_TOOLTIP_ADD"] = "Ist die Summe zweier Zahlen.";
Blockly.Msg["MATH_ARITHMETIC_TOOLTIP_DIVIDE"] = "Ist der Quotient zweier Zahlen.";
Blockly.Msg["MATH_ARITHMETIC_TOOLTIP_MINUS"] = "Ist die Differenz zweier Zahlen.";
Blockly.Msg["MATH_ARITHMETIC_TOOLTIP_MULTIPLY"] = "Ist das Produkt zweier Zahlen.";
Blockly.Msg["MATH_ARITHMETIC_TOOLTIP_POWER"] = "Ist die erste Zahl potenziert mit der zweiten Zahl.";
Blockly.Msg["MATH_ATAN2_HELPURL"] = "https://de.wikipedia.org/wiki/Arctan2";
Blockly.Msg["MATH_ATAN2_TITLE"] = "atan2 von X:%1 Y:%2";
Blockly.Msg["MATH_ATAN2_TOOLTIP"] = "Gibt den Arkustangens des Punktes (X, Y) in Grad von -180 bis 180 zurück.";
Blockly.Msg["MATH_CHANGE_HELPURL"] = "https://de.wikipedia.org/wiki/Inkrement_und_Dekrement";
Blockly.Msg["MATH_CHANGE_TITLE"] = "erhöhe %1 um %2";
Blockly.Msg["MATH_CHANGE_TOOLTIP"] = "Addiert eine Zahl zur Variable „%1“.";
Blockly.Msg["MATH_CONSTANT_HELPURL"] = "https://de.wikipedia.org/wiki/Mathematische_Konstante";
Blockly.Msg["MATH_CONSTANT_TOOLTIP"] = "Mathematische Konstanten wie: π (3.141…), e (2.718…), φ (1.618…), sqrt(2) (1.414…), sqrt(½) (0.707…) oder ∞ (unendlich).";
Blockly.Msg["MATH_CONSTRAIN_HELPURL"] = "https://en.wikipedia.org/wiki/Clamping_(graphics)"; // untranslated
Blockly.Msg["MATH_CONSTRAIN_TITLE"] = "begrenze %1 zwischen %2 und %3";
Blockly.Msg["MATH_CONSTRAIN_TOOLTIP"] = "Begrenzt eine Zahl auf den Wertebereich zwischen zwei anderen Zahlen (inklusiv).";
Blockly.Msg["MATH_DIVISION_SYMBOL"] = "÷"; // untranslated
Blockly.Msg["MATH_IS_DIVISIBLE_BY"] = "ist teilbar durch";
Blockly.Msg["MATH_IS_EVEN"] = "ist gerade";
Blockly.Msg["MATH_IS_NEGATIVE"] = "ist negativ";
Blockly.Msg["MATH_IS_ODD"] = "ist ungerade";
Blockly.Msg["MATH_IS_POSITIVE"] = "ist positiv";
Blockly.Msg["MATH_IS_PRIME"] = "ist eine Primzahl";
Blockly.Msg["MATH_IS_TOOLTIP"] = "Überprüft, ob eine Zahl gerade, ungerade, eine Primzahl, ganzzahlig, positiv, negativ oder durch eine zweite Zahl teilbar ist. Gibt wahr oder falsch zurück.";
Blockly.Msg["MATH_IS_WHOLE"] = "ist eine ganze Zahl";
Blockly.Msg["MATH_MODULO_HELPURL"] = "https://de.wikipedia.org/wiki/Modulo";
Blockly.Msg["MATH_MODULO_TITLE"] = "Rest von %1 ÷ %2";
Blockly.Msg["MATH_MODULO_TOOLTIP"] = "Der Rest nach einer Division.";
Blockly.Msg["MATH_MULTIPLICATION_SYMBOL"] = "×"; // untranslated
Blockly.Msg["MATH_NUMBER_HELPURL"] = "https://de.wikipedia.org/wiki/Zahl";
Blockly.Msg["MATH_NUMBER_TOOLTIP"] = "Eine Zahl.";
Blockly.Msg["MATH_ONLIST_HELPURL"] = "";
Blockly.Msg["MATH_ONLIST_OPERATOR_AVERAGE"] = "Mittelwert der Liste";
Blockly.Msg["MATH_ONLIST_OPERATOR_MAX"] = "Maximalwert der Liste";
Blockly.Msg["MATH_ONLIST_OPERATOR_MEDIAN"] = "Median der Liste";
Blockly.Msg["MATH_ONLIST_OPERATOR_MIN"] = "Minimalwert der Liste";
Blockly.Msg["MATH_ONLIST_OPERATOR_MODE"] = "am häufigsten in der Liste";
Blockly.Msg["MATH_ONLIST_OPERATOR_RANDOM"] = "Zufallswert aus der Liste";
Blockly.Msg["MATH_ONLIST_OPERATOR_STD_DEV"] = "Standardabweichung der Liste";
Blockly.Msg["MATH_ONLIST_OPERATOR_SUM"] = "Summe über die Liste";
Blockly.Msg["MATH_ONLIST_TOOLTIP_AVERAGE"] = "Ist der Durchschnittswert aller Zahlen in einer Liste.";
Blockly.Msg["MATH_ONLIST_TOOLTIP_MAX"] = "Ist die größte Zahl in einer Liste.";
Blockly.Msg["MATH_ONLIST_TOOLTIP_MEDIAN"] = "Ist der Median aller Zahlen in einer Liste.";
Blockly.Msg["MATH_ONLIST_TOOLTIP_MIN"] = "Ist die kleinste Zahl in einer Liste.";
Blockly.Msg["MATH_ONLIST_TOOLTIP_MODE"] = "Findet die Werte mit dem häufigsten Vorkommen in der Liste.";
Blockly.Msg["MATH_ONLIST_TOOLTIP_RANDOM"] = "Gibt einen zufälligen Wert aus der Liste zurück.";
Blockly.Msg["MATH_ONLIST_TOOLTIP_STD_DEV"] = "Ist die Standardabweichung aller Werte in der Liste.";
Blockly.Msg["MATH_ONLIST_TOOLTIP_SUM"] = "Ist die Summe aller Zahlen in einer Liste.";
Blockly.Msg["MATH_POWER_SYMBOL"] = "^"; // untranslated
Blockly.Msg["MATH_RANDOM_FLOAT_HELPURL"] = "https://de.wikipedia.org/wiki/Zufallszahlen";
Blockly.Msg["MATH_RANDOM_FLOAT_TITLE_RANDOM"] = "Zufallszahl (0.0 - 1.0)";
Blockly.Msg["MATH_RANDOM_FLOAT_TOOLTIP"] = "Erzeugt eine Zufallszahl zwischen 0.0 (inklusiv) und 1.0 (exklusiv).";
Blockly.Msg["MATH_RANDOM_INT_HELPURL"] = "https://de.wikipedia.org/wiki/Zufallszahlen";
Blockly.Msg["MATH_RANDOM_INT_TITLE"] = "ganzzahlige Zufallszahl zwischen %1 und %2";
Blockly.Msg["MATH_RANDOM_INT_TOOLTIP"] = "Erzeugt eine ganzzahlige Zufallszahl zwischen zwei Zahlen (inklusiv).";
Blockly.Msg["MATH_ROUND_HELPURL"] = "https://de.wikipedia.org/wiki/Runden";
Blockly.Msg["MATH_ROUND_OPERATOR_ROUND"] = "runde";
Blockly.Msg["MATH_ROUND_OPERATOR_ROUNDDOWN"] = "runde ab";
Blockly.Msg["MATH_ROUND_OPERATOR_ROUNDUP"] = "runde auf";
Blockly.Msg["MATH_ROUND_TOOLTIP"] = "Eine Zahl auf- oder abrunden.";
Blockly.Msg["MATH_SINGLE_HELPURL"] = "https://de.wikipedia.org/wiki/Quadratwurzel";
Blockly.Msg["MATH_SINGLE_OP_ABSOLUTE"] = "Absolutbetrag";
Blockly.Msg["MATH_SINGLE_OP_ROOT"] = "Quadratwurzel";
Blockly.Msg["MATH_SINGLE_TOOLTIP_ABS"] = "Ist der Absolutbetrag einer Zahl.";
Blockly.Msg["MATH_SINGLE_TOOLTIP_EXP"] = "Ist Wert der Exponentialfunktion einer Zahl.";
Blockly.Msg["MATH_SINGLE_TOOLTIP_LN"] = "Ist der natürliche Logarithmus einer Zahl.";
Blockly.Msg["MATH_SINGLE_TOOLTIP_LOG10"] = "Ist der dekadische Logarithmus einer Zahl.";
Blockly.Msg["MATH_SINGLE_TOOLTIP_NEG"] = "Negiert eine Zahl.";
Blockly.Msg["MATH_SINGLE_TOOLTIP_POW10"] = "Rechnet 10 hoch eine Zahl.";
Blockly.Msg["MATH_SINGLE_TOOLTIP_ROOT"] = "Ist die Quadratwurzel einer Zahl.";
Blockly.Msg["MATH_SUBTRACTION_SYMBOL"] = "-"; // untranslated
Blockly.Msg["MATH_TRIG_ACOS"] = "acos";
Blockly.Msg["MATH_TRIG_ASIN"] = "asin";
Blockly.Msg["MATH_TRIG_ATAN"] = "atan";
Blockly.Msg["MATH_TRIG_COS"] = "cos";
Blockly.Msg["MATH_TRIG_HELPURL"] = "https://de.wikipedia.org/wiki/Trigonometrie";
Blockly.Msg["MATH_TRIG_SIN"] = "sin";
Blockly.Msg["MATH_TRIG_TAN"] = "tan";
Blockly.Msg["MATH_TRIG_TOOLTIP_ACOS"] = "Ist der Arkuskosinus des Eingabewertes.";
Blockly.Msg["MATH_TRIG_TOOLTIP_ASIN"] = "Ist der Arkussinus des Eingabewertes.";
Blockly.Msg["MATH_TRIG_TOOLTIP_ATAN"] = "Ist der Arkustangens des Eingabewertes.";
Blockly.Msg["MATH_TRIG_TOOLTIP_COS"] = "Ist der Kosinus des Winkels (nicht Radiant).";
Blockly.Msg["MATH_TRIG_TOOLTIP_SIN"] = "Ist der Sinus des Winkels (nicht Radiant).";
Blockly.Msg["MATH_TRIG_TOOLTIP_TAN"] = "Ist der Tangens des Winkels (nicht Radiant).";
Blockly.Msg["MOVE_BLOCK"] = "Move Block"; // untranslated
Blockly.Msg["NEW_COLOUR_VARIABLE"] = "Farbvariable erstellen …";
Blockly.Msg["NEW_NUMBER_VARIABLE"] = "Zahlenvariable erstellen …";
Blockly.Msg["NEW_STRING_VARIABLE"] = "Zeichenfolgenvariable erstellen …";
Blockly.Msg["NEW_VARIABLE"] = "Variable erstellen …";
Blockly.Msg["NEW_VARIABLE_TITLE"] = "Name der neuen Variable:";
Blockly.Msg["NEW_VARIABLE_TYPE_TITLE"] = "Neuer Variablentyp:";
Blockly.Msg["OPTION_KEY"] = "⌥ Option"; // untranslated
Blockly.Msg["ORDINAL_NUMBER_SUFFIX"] = ".";
Blockly.Msg["PASTE_SHORTCUT"] = "Paste"; // untranslated
Blockly.Msg["PROCEDURES_ALLOW_STATEMENTS"] = "Anweisungen erlauben";
Blockly.Msg["PROCEDURES_BEFORE_PARAMS"] = "mit:";
Blockly.Msg["PROCEDURES_CALLNORETURN_HELPURL"] = "https://de.wikipedia.org/wiki/Unterprogramm";
Blockly.Msg["PROCEDURES_CALLNORETURN_TOOLTIP"] = "Ruft die benutzerdefinierte Funktion „%1“ auf.";
Blockly.Msg["PROCEDURES_CALLRETURN_HELPURL"] = "https://de.wikipedia.org/wiki/Unterprogramm";
Blockly.Msg["PROCEDURES_CALLRETURN_TOOLTIP"] = "Ruft die benutzerdefinierte Funktion „%1“ auf und verwendet ihre Ausgabe.";
Blockly.Msg["PROCEDURES_CALL_BEFORE_PARAMS"] = "mit:";
Blockly.Msg["PROCEDURES_CALL_DISABLED_DEF_WARNING"] = "Die benutzerdefinierte Funktion '%1' kann nicht ausgeführt werden, weil der Definitionsblock deaktiviert ist.";
Blockly.Msg["PROCEDURES_CREATE_DO"] = "Erzeuge \"Aufruf %1\"";
Blockly.Msg["PROCEDURES_DEFNORETURN_COMMENT"] = "Beschreibe diese Funktion …";
Blockly.Msg["PROCEDURES_DEFNORETURN_DO"] = ""; // untranslated
Blockly.Msg["PROCEDURES_DEFNORETURN_HELPURL"] = "https://en.wikipedia.org/wiki/Subroutine"; // untranslated
Blockly.Msg["PROCEDURES_DEFNORETURN_PROCEDURE"] = "etwas tun";
Blockly.Msg["PROCEDURES_DEFNORETURN_TITLE"] = "um";
Blockly.Msg["PROCEDURES_DEFNORETURN_TOOLTIP"] = "Ein Funktionsblock ohne Rückgabewert.";
Blockly.Msg["PROCEDURES_DEFRETURN_HELPURL"] = "https://de.wikipedia.org/wiki/Prozedur_(Programmierung)";
Blockly.Msg["PROCEDURES_DEFRETURN_RETURN"] = "gib zurück";
Blockly.Msg["PROCEDURES_DEFRETURN_TOOLTIP"] = "Ein Funktionsblock mit Rückgabewert.";
Blockly.Msg["PROCEDURES_DEF_DUPLICATE_WARNING"] = "Warnung: Dieser Funktionsblock hat zwei gleich benannte Parameter.";
Blockly.Msg["PROCEDURES_HIGHLIGHT_DEF"] = "Markiere Funktionsblock";
Blockly.Msg["PROCEDURES_IFRETURN_HELPURL"] = "https://c2.com/cgi/wiki?GuardClause"; // untranslated
Blockly.Msg["PROCEDURES_IFRETURN_TOOLTIP"] = "Gibt den zweiten Wert zurück und verlässt die Funktion, falls der erste Wert wahr ist.";
Blockly.Msg["PROCEDURES_IFRETURN_WARNING"] = "Warnung: Dieser Block darf nur innerhalb eines Funktionsblocks genutzt werden.";
Blockly.Msg["PROCEDURES_MUTATORARG_TITLE"] = "Variable:";
Blockly.Msg["PROCEDURES_MUTATORARG_TOOLTIP"] = "Eine Eingabe zur Funktion hinzufügen.";
Blockly.Msg["PROCEDURES_MUTATORCONTAINER_TITLE"] = "Parameter";
Blockly.Msg["PROCEDURES_MUTATORCONTAINER_TOOLTIP"] = "Die Eingaben zu dieser Funktion hinzufügen, entfernen oder neu anordnen.";
Blockly.Msg["REDO"] = "Wiederholen";
Blockly.Msg["REMOVE_COMMENT"] = "Kommentar entfernen";
Blockly.Msg["RENAME_VARIABLE"] = "Variable umbenennen …";
Blockly.Msg["RENAME_VARIABLE_TITLE"] = "Alle „%1“-Variablen umbenennen in:";
Blockly.Msg["SHORTCUTS_CODE_NAVIGATION"] = "Code navigation"; // untranslated
Blockly.Msg["SHORTCUTS_EDITING"] = "Editing"; // untranslated
Blockly.Msg["SHORTCUTS_GENERAL"] = "General"; // untranslated
Blockly.Msg["TEXT_APPEND_HELPURL"] = "https://github.com/google/blockly/wiki/Text#text-modification"; // untranslated
Blockly.Msg["TEXT_APPEND_TITLE"] = "zu %1 Text %2 anhängen";
Blockly.Msg["TEXT_APPEND_TOOLTIP"] = "Text an die Variable \"%1\" anhängen.";
Blockly.Msg["TEXT_CHANGECASE_HELPURL"] = "https://github.com/google/blockly/wiki/Text#adjusting-text-case"; // untranslated
Blockly.Msg["TEXT_CHANGECASE_OPERATOR_LOWERCASE"] = "wandel um in kleinbuchstaben";
Blockly.Msg["TEXT_CHANGECASE_OPERATOR_TITLECASE"] = "wandel um in Substantive";
Blockly.Msg["TEXT_CHANGECASE_OPERATOR_UPPERCASE"] = "wandel um in GROSSBUCHSTABEN";
Blockly.Msg["TEXT_CHANGECASE_TOOLTIP"] = "Wandelt Schreibweise von Texten um, in Großbuchstaben, Kleinbuchstaben oder den ersten Buchstaben jedes Wortes groß und die anderen klein.";
Blockly.Msg["TEXT_CHARAT_FIRST"] = "nimm ersten";
Blockly.Msg["TEXT_CHARAT_FROM_END"] = "nimm von hinten";
Blockly.Msg["TEXT_CHARAT_FROM_START"] = "nimm";
Blockly.Msg["TEXT_CHARAT_HELPURL"] = "https://github.com/google/blockly/wiki/Text#extracting-text"; // untranslated
Blockly.Msg["TEXT_CHARAT_LAST"] = "nimm letzten";
Blockly.Msg["TEXT_CHARAT_RANDOM"] = "nimm zufälligen";
Blockly.Msg["TEXT_CHARAT_TAIL"] = "Buchstaben";
Blockly.Msg["TEXT_CHARAT_TITLE"] = "im Text %1 %2";
Blockly.Msg["TEXT_CHARAT_TOOLTIP"] = "Extrahiert einen Buchstaben von einer bestimmten Position.";
Blockly.Msg["TEXT_COUNT_HELPURL"] = "https://github.com/google/blockly/wiki/Text#counting-substrings"; // untranslated
Blockly.Msg["TEXT_COUNT_MESSAGE0"] = "zähle %1 in %2";
Blockly.Msg["TEXT_COUNT_TOOLTIP"] = "Zähle, wie oft ein Text innerhalb eines anderen Textes vorkommt.";
Blockly.Msg["TEXT_CREATE_JOIN_ITEM_TOOLTIP"] = "Ein Element zum Text hinzufügen.";
Blockly.Msg["TEXT_CREATE_JOIN_TITLE_JOIN"] = "verbinden";
Blockly.Msg["TEXT_CREATE_JOIN_TOOLTIP"] = "Hinzufügen, entfernen und sortieren von Elementen.";
Blockly.Msg["TEXT_GET_SUBSTRING_END_FROM_END"] = "bis von hinten";
Blockly.Msg["TEXT_GET_SUBSTRING_END_FROM_START"] = "bis";
Blockly.Msg["TEXT_GET_SUBSTRING_END_LAST"] = "bis letzter";
Blockly.Msg["TEXT_GET_SUBSTRING_HELPURL"] = "https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text"; // untranslated
Blockly.Msg["TEXT_GET_SUBSTRING_INPUT_IN_TEXT"] = "im Text";
Blockly.Msg["TEXT_GET_SUBSTRING_START_FIRST"] = "nimm Teil ab erster";
Blockly.Msg["TEXT_GET_SUBSTRING_START_FROM_END"] = "nimm Teil ab von hinten";
Blockly.Msg["TEXT_GET_SUBSTRING_START_FROM_START"] = "nimm Teil ab";
Blockly.Msg["TEXT_GET_SUBSTRING_TAIL"] = "Buchstabe";
Blockly.Msg["TEXT_GET_SUBSTRING_TOOLTIP"] = "Gibt den angegebenen Textabschnitt zurück.";
Blockly.Msg["TEXT_INDEXOF_HELPURL"] = "https://github.com/google/blockly/wiki/Text#finding-text"; // untranslated
Blockly.Msg["TEXT_INDEXOF_OPERATOR_FIRST"] = "suche erstes Auftreten des Begriffs";
Blockly.Msg["TEXT_INDEXOF_OPERATOR_LAST"] = "suche letztes Auftreten des Begriffs";
Blockly.Msg["TEXT_INDEXOF_TITLE"] = "im Text %1 %2 %3";
Blockly.Msg["TEXT_INDEXOF_TOOLTIP"] = "Findet das erste / letzte Auftreten eines Suchbegriffs in einem Text. Gibt die Position des Begriffs zurück oder %1, falls der Suchbegriff nicht gefunden wurde.";
Blockly.Msg["TEXT_ISEMPTY_HELPURL"] = "https://github.com/google/blockly/wiki/Text#checking-for-empty-text"; // untranslated
Blockly.Msg["TEXT_ISEMPTY_TITLE"] = "%1 ist leer";
Blockly.Msg["TEXT_ISEMPTY_TOOLTIP"] = "Ist wahr, falls der Text keine Zeichen enthält.";
Blockly.Msg["TEXT_JOIN_HELPURL"] = "https://github.com/google/blockly/wiki/Text#text-creation"; // untranslated
Blockly.Msg["TEXT_JOIN_TITLE_CREATEWITH"] = "erstelle Text aus";
Blockly.Msg["TEXT_JOIN_TOOLTIP"] = "Erstellt einen Text durch das Verbinden von mehreren Textelementen.";
Blockly.Msg["TEXT_LENGTH_HELPURL"] = "https://github.com/google/blockly/wiki/Text#text-modification"; // untranslated
Blockly.Msg["TEXT_LENGTH_TITLE"] = "Länge von %1";
Blockly.Msg["TEXT_LENGTH_TOOLTIP"] = "Die Anzahl von Zeichen in einem Text (inkl. Leerzeichen).";
Blockly.Msg["TEXT_PRINT_HELPURL"] = "https://github.com/google/blockly/wiki/Text#printing-text"; // untranslated
Blockly.Msg["TEXT_PRINT_TITLE"] = "gib aus %1";
Blockly.Msg["TEXT_PRINT_TOOLTIP"] = "Gibt den Text aus.";
Blockly.Msg["TEXT_PROMPT_HELPURL"] = "https://github.com/google/blockly/wiki/Text#getting-input-from-the-user"; // untranslated
Blockly.Msg["TEXT_PROMPT_TOOLTIP_NUMBER"] = "Fragt den Benutzer nach einer Zahl.";
Blockly.Msg["TEXT_PROMPT_TOOLTIP_TEXT"] = "Fragt den Benutzer nach einem Text.";
Blockly.Msg["TEXT_PROMPT_TYPE_NUMBER"] = "frage nach Zahl mit Hinweis";
Blockly.Msg["TEXT_PROMPT_TYPE_TEXT"] = "frage nach Text mit Hinweis";
Blockly.Msg["TEXT_REPLACE_HELPURL"] = "https://github.com/google/blockly/wiki/Text#replacing-substrings"; // untranslated
Blockly.Msg["TEXT_REPLACE_MESSAGE0"] = "ersetze %1 durch %2 in %3";
Blockly.Msg["TEXT_REPLACE_TOOLTIP"] = "Ersetze alle Vorkommen eines Textes innerhalb eines anderen Textes.";
Blockly.Msg["TEXT_REVERSE_HELPURL"] = "https://github.com/google/blockly/wiki/Text#reversing-text"; // untranslated
Blockly.Msg["TEXT_REVERSE_MESSAGE0"] = "kehre %1 um";
Blockly.Msg["TEXT_REVERSE_TOOLTIP"] = "Kehre die Reihenfolge der Zeichen im Text um.";
Blockly.Msg["TEXT_TEXT_HELPURL"] = "https://de.wikipedia.org/wiki/Zeichenkette";
Blockly.Msg["TEXT_TEXT_TOOLTIP"] = "Ein Buchstabe, Text oder Satz.";
Blockly.Msg["TEXT_TRIM_HELPURL"] = "https://github.com/google/blockly/wiki/Text#trimming-removing-spaces"; // untranslated
Blockly.Msg["TEXT_TRIM_OPERATOR_BOTH"] = "entferne Leerzeichen vom Anfang und vom Ende (links und rechts)";
Blockly.Msg["TEXT_TRIM_OPERATOR_LEFT"] = "entferne Leerzeichen vom Anfang (links)";
Blockly.Msg["TEXT_TRIM_OPERATOR_RIGHT"] = "entferne Leerzeichen vom Ende (rechts)";
Blockly.Msg["TEXT_TRIM_TOOLTIP"] = "Entfernt Leerzeichen vom Anfang und / oder Ende eines Textes.";
Blockly.Msg["TODAY"] = "Heute";
Blockly.Msg["UNDO"] = "Rückgängig";
Blockly.Msg["UNKNOWN"] = "Unknown"; // untranslated
Blockly.Msg["UNNAMED_KEY"] = "unbenannt";
Blockly.Msg["VARIABLES_DEFAULT_NAME"] = "Element";
Blockly.Msg["VARIABLES_GET_CREATE_SET"] = "Erzeuge \"Schreibe %1\"";
Blockly.Msg["VARIABLES_GET_HELPURL"] = "https://github.com/google/blockly/wiki/Variables#get"; // untranslated
Blockly.Msg["VARIABLES_GET_TOOLTIP"] = "Gibt den Wert der Variable zurück.";
Blockly.Msg["VARIABLES_SET"] = "setze %1 auf %2";
Blockly.Msg["VARIABLES_SET_CREATE_GET"] = "Erzeuge \"Lies %1\"";
Blockly.Msg["VARIABLES_SET_HELPURL"] = "https://github.com/google/blockly/wiki/Variables#set"; // untranslated
Blockly.Msg["VARIABLES_SET_TOOLTIP"] = "Setzt den Wert einer Variable.";
Blockly.Msg["VARIABLE_ALREADY_EXISTS"] = "Eine Variable namens „%1“ ist bereits vorhanden.";
Blockly.Msg["VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE"] = "Eine Variable namens „%1“ ist bereits für einen anderen Typ vorhanden: „%2“.";
Blockly.Msg["VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER"] = "Eine Variable mit dem Namen „%1“ ist bereits als Parameter in der Funktion „%2“ vorhanden.";
Blockly.Msg["WINDOWS"] = "Windows"; // untranslated
Blockly.Msg["WORKSPACE_ARIA_LABEL"] = "Blockly-Arbeitsbereich";
Blockly.Msg["WORKSPACE_COMMENT_DEFAULT_TEXT"] = "Teile etwas mit…";
Blockly.Msg["CONTROLS_FOREACH_INPUT_DO"] = Blockly.Msg["CONTROLS_REPEAT_INPUT_DO"];
Blockly.Msg["CONTROLS_FOR_INPUT_DO"] = Blockly.Msg["CONTROLS_REPEAT_INPUT_DO"];
Blockly.Msg["CONTROLS_IF_ELSEIF_TITLE_ELSEIF"] = Blockly.Msg["CONTROLS_IF_MSG_ELSEIF"];
Blockly.Msg["CONTROLS_IF_ELSE_TITLE_ELSE"] = Blockly.Msg["CONTROLS_IF_MSG_ELSE"];
Blockly.Msg["CONTROLS_IF_IF_TITLE_IF"] = Blockly.Msg["CONTROLS_IF_MSG_IF"];
Blockly.Msg["CONTROLS_IF_MSG_THEN"] = Blockly.Msg["CONTROLS_REPEAT_INPUT_DO"];
Blockly.Msg["CONTROLS_WHILEUNTIL_INPUT_DO"] = Blockly.Msg["CONTROLS_REPEAT_INPUT_DO"];
Blockly.Msg["LISTS_CREATE_WITH_ITEM_TITLE"] = Blockly.Msg["VARIABLES_DEFAULT_NAME"];
Blockly.Msg["LISTS_GET_INDEX_INPUT_IN_LIST"] = Blockly.Msg["LISTS_INLIST"];
Blockly.Msg["LISTS_GET_SUBLIST_INPUT_IN_LIST"] = Blockly.Msg["LISTS_INLIST"];
Blockly.Msg["LISTS_INDEX_OF_INPUT_IN_LIST"] = Blockly.Msg["LISTS_INLIST"];
Blockly.Msg["LISTS_SET_INDEX_INPUT_IN_LIST"] = Blockly.Msg["LISTS_INLIST"];
Blockly.Msg["MATH_CHANGE_TITLE_ITEM"] = Blockly.Msg["VARIABLES_DEFAULT_NAME"];
Blockly.Msg["PROCEDURES_DEFRETURN_COMMENT"] = Blockly.Msg["PROCEDURES_DEFNORETURN_COMMENT"];
Blockly.Msg["PROCEDURES_DEFRETURN_DO"] = Blockly.Msg["PROCEDURES_DEFNORETURN_DO"];
Blockly.Msg["PROCEDURES_DEFRETURN_PROCEDURE"] = Blockly.Msg["PROCEDURES_DEFNORETURN_PROCEDURE"];
Blockly.Msg["PROCEDURES_DEFRETURN_TITLE"] = Blockly.Msg["PROCEDURES_DEFNORETURN_TITLE"];
Blockly.Msg["TEXT_APPEND_VARIABLE"] = Blockly.Msg["VARIABLES_DEFAULT_NAME"];
Blockly.Msg["TEXT_CREATE_JOIN_ITEM_TITLE_ITEM"] = Blockly.Msg["VARIABLES_DEFAULT_NAME"];
Blockly.Msg["COLOUR_HUE"] = "20";
Blockly.Msg["LISTS_HUE"] = "260";
Blockly.Msg["LOGIC_HUE"] = "210";
Blockly.Msg["LOOPS_HUE"] = "120";
Blockly.Msg["MATH_HUE"] = "230";
Blockly.Msg["PROCEDURES_HUE"] = "290";
Blockly.Msg["TEXTS_HUE"] = "160";
Blockly.Msg["VARIABLES_DYNAMIC_HUE"] = "310";
Blockly.Msg["VARIABLES_HUE"] = "330";
return Blockly.Msg;
}));

View File

@@ -0,0 +1,264 @@
// Do not edit this file; automatically generated.
/* eslint-disable */
;(function(root, factory) {
if (typeof define === 'function' && define.amd) { // AMD
define(["./blockly_compressed.js"], factory);
} else if (typeof exports === 'object') { // Node.js
module.exports = factory(require("./blockly_compressed.js"));
} else { // Script
root.javascript = factory(root.Blockly);
root.Blockly.JavaScript = root.javascript.javascriptGenerator;
}
}(this, function(__parent__) {
var $=__parent__.__namespace__;
var lists_create_empty$$module$build$src$generators$javascript$lists=function(a,b){return["[]",Order$$module$build$src$generators$javascript$javascript_generator.ATOMIC]},lists_create_with$$module$build$src$generators$javascript$lists=function(a,b){const c=Array(a.itemCount_);for(let d=0;d<a.itemCount_;d++)c[d]=b.valueToCode(a,"ADD"+d,Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"null";return["["+c.join(", ")+"]",Order$$module$build$src$generators$javascript$javascript_generator.ATOMIC]},
lists_repeat$$module$build$src$generators$javascript$lists=function(a,b){const c=b.provideFunction_("listsRepeat",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(value, n) {
var array = [];
for (var i = 0; i < n; i++) {
array[i] = value;
}
return array;
}
`),d=b.valueToCode(a,"ITEM",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"null";a=b.valueToCode(a,"NUM",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"0";return[c+"("+d+", "+a+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},lists_length$$module$build$src$generators$javascript$lists=function(a,b){return[(b.valueToCode(a,"VALUE",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||
"[]")+".length",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER]},lists_isEmpty$$module$build$src$generators$javascript$lists=function(a,b){return["!"+(b.valueToCode(a,"VALUE",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"[]")+".length",Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_NOT]},lists_indexOf$$module$build$src$generators$javascript$lists=function(a,b){const c=a.getFieldValue("END")==="FIRST"?"indexOf":"lastIndexOf",
d=b.valueToCode(a,"FIND",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";b=(b.valueToCode(a,"VALUE",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"[]")+"."+c+"("+d+")";return a.workspace.options.oneBasedIndex?[b+" + 1",Order$$module$build$src$generators$javascript$javascript_generator.ADDITION]:[b,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},lists_getIndex$$module$build$src$generators$javascript$lists=
function(a,b){const c=a.getFieldValue("MODE")||"GET",d=a.getFieldValue("WHERE")||"FROM_START";var e=b.valueToCode(a,"VALUE",d==="RANDOM"?Order$$module$build$src$generators$javascript$javascript_generator.NONE:Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"[]";switch(d){case "FIRST":if(c==="GET")return[e+"[0]",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER];if(c==="GET_REMOVE")return[e+".shift()",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER];
if(c==="REMOVE")return e+".shift();\n";break;case "LAST":if(c==="GET")return[e+".slice(-1)[0]",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER];if(c==="GET_REMOVE")return[e+".pop()",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER];if(c==="REMOVE")return e+".pop();\n";break;case "FROM_START":a=b.getAdjusted(a,"AT");if(c==="GET")return[e+"["+a+"]",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER];if(c==="GET_REMOVE")return[e+
".splice("+a+", 1)[0]",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL];if(c==="REMOVE")return e+".splice("+a+", 1);\n";break;case "FROM_END":a=b.getAdjusted(a,"AT",1,!0);if(c==="GET")return[e+".slice("+a+")[0]",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL];if(c==="GET_REMOVE")return[e+".splice("+a+", 1)[0]",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL];if(c==="REMOVE")return e+".splice("+a+", 1);";
break;case "RANDOM":e=b.provideFunction_("listsGetRandomItem",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(list, remove) {
var x = Math.floor(Math.random() * list.length);
if (remove) {
return list.splice(x, 1)[0];
} else {
return list[x];
}
}
`)+"("+e+", "+(c!=="GET")+")";if(c==="GET"||c==="GET_REMOVE")return[e,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL];if(c==="REMOVE")return e+";\n"}throw Error("Unhandled combination (lists_getIndex).");},lists_setIndex$$module$build$src$generators$javascript$lists=function(a,b){function c(){if(d.match(/^\w+$/))return"";const h=b.nameDB_.getDistinctName("tmpList",$.NameType$$module$build$src$core$names.VARIABLE),k="var "+h+" = "+d+";\n";d=h;return k}let d=b.valueToCode(a,
"LIST",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"[]";const e=a.getFieldValue("MODE")||"GET";var f=a.getFieldValue("WHERE")||"FROM_START";const g=b.valueToCode(a,"TO",Order$$module$build$src$generators$javascript$javascript_generator.ASSIGNMENT)||"null";switch(f){case "FIRST":if(e==="SET")return d+"[0] = "+g+";\n";if(e==="INSERT")return d+".unshift("+g+");\n";break;case "LAST":if(e==="SET")return c()+(d+"["+d+".length - 1] = "+g+";\n");if(e==="INSERT")return d+".push("+
g+");\n";break;case "FROM_START":a=b.getAdjusted(a,"AT");if(e==="SET")return d+"["+a+"] = "+g+";\n";if(e==="INSERT")return d+".splice("+a+", 0, "+g+");\n";break;case "FROM_END":a=b.getAdjusted(a,"AT",1,!1,Order$$module$build$src$generators$javascript$javascript_generator.SUBTRACTION);f=c();if(e==="SET")return f+(d+"["+d+".length - "+a+"] = "+g+";\n");if(e==="INSERT")return f+(d+".splice("+d+".length - "+a+", 0, "+g+");\n");break;case "RANDOM":a=c();f=b.nameDB_.getDistinctName("tmpX",$.NameType$$module$build$src$core$names.VARIABLE);
a+="var "+f+" = Math.floor(Math.random() * "+d+".length);\n";if(e==="SET")return a+(d+"["+f+"] = "+g+";\n");if(e==="INSERT")return a+(d+".splice("+f+", 0, "+g+");\n")}throw Error("Unhandled combination (lists_setIndex).");},lists_getSublist$$module$build$src$generators$javascript$lists=function(a,b){var c={FIRST:"First",LAST:"Last",FROM_START:"FromStart",FROM_END:"FromEnd"},d=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"[]";const e=a.getFieldValue("WHERE1"),
f=a.getFieldValue("WHERE2");if(e==="FIRST"&&f==="LAST")d+=".slice(0)";else if(d.match(/^\w+$/)||e!=="FROM_END"&&f==="FROM_START"){switch(e){case "FROM_START":c=b.getAdjusted(a,"AT1");break;case "FROM_END":c=b.getAdjusted(a,"AT1",1,!1,Order$$module$build$src$generators$javascript$javascript_generator.SUBTRACTION);c=d+".length - "+c;break;case "FIRST":c="0";break;default:throw Error("Unhandled option (lists_getSublist).");}switch(f){case "FROM_START":b=b.getAdjusted(a,"AT2",1);break;case "FROM_END":b=
b.getAdjusted(a,"AT2",0,!1,Order$$module$build$src$generators$javascript$javascript_generator.SUBTRACTION);b=d+".length - "+b;break;case "LAST":b=d+".length";break;default:throw Error("Unhandled option (lists_getSublist).");}d=d+".slice("+c+", "+b+")"}else{const g=b.getAdjusted(a,"AT1");a=b.getAdjusted(a,"AT2");d=b.provideFunction_("subsequence"+c[e]+c[f],`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(sequence${e==="FROM_END"||e==="FROM_START"?", at1":""}${f==="FROM_END"||f==="FROM_START"?", at2":""}) {
var start = ${getSubstringIndex$$module$build$src$generators$javascript$lists("sequence",e,"at1")};
var end = ${getSubstringIndex$$module$build$src$generators$javascript$lists("sequence",f,"at2")} + 1;
return sequence.slice(start, end);
}
`)+"("+d+(e==="FROM_END"||e==="FROM_START"?", "+g:"")+(f==="FROM_END"||f==="FROM_START"?", "+a:"")+")"}return[d,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},lists_sort$$module$build$src$generators$javascript$lists=function(a,b){const c=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL)||"[]",d=a.getFieldValue("DIRECTION")==="1"?1:-1;a=a.getFieldValue("TYPE");b=b.provideFunction_("listsGetSortCompare",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(type, direction) {
var compareFuncs = {
'NUMERIC': function(a, b) {
return Number(a) - Number(b); },
'TEXT': function(a, b) {
return String(a) > String(b) ? 1 : -1; },
'IGNORE_CASE': function(a, b) {
return String(a).toLowerCase() > String(b).toLowerCase() ? 1 : -1; },
};
var compare = compareFuncs[type];
return function(a, b) { return compare(a, b) * direction; };
}
`);return[c+".slice().sort("+b+'("'+a+'", '+d+"))",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},lists_split$$module$build$src$generators$javascript$lists=function(a,b){let c=b.valueToCode(a,"INPUT",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER);b=b.valueToCode(a,"DELIM",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";a=a.getFieldValue("MODE");if(a==="SPLIT")c||(c="''"),a="split";else if(a==="JOIN")c||
(c="[]"),a="join";else throw Error("Unknown mode: "+a);return[c+"."+a+"("+b+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},lists_reverse$$module$build$src$generators$javascript$lists=function(a,b){return[(b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL)||"[]")+".slice().reverse()",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},controls_if$$module$build$src$generators$javascript$logic=
function(a,b){var c=0;let d="";b.STATEMENT_PREFIX&&(d+=b.injectId(b.STATEMENT_PREFIX,a));do{const e=b.valueToCode(a,"IF"+c,Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"false";let f=b.statementToCode(a,"DO"+c);b.STATEMENT_SUFFIX&&(f=b.prefixLines(b.injectId(b.STATEMENT_SUFFIX,a),b.INDENT)+f);d+=(c>0?" else ":"")+"if ("+e+") {\n"+f+"}";c++}while(a.getInput("IF"+c));if(a.getInput("ELSE")||b.STATEMENT_SUFFIX)c=a.getInput("ELSE")?b.statementToCode(a,"ELSE"):"",b.STATEMENT_SUFFIX&&
(c=b.prefixLines(b.injectId(b.STATEMENT_SUFFIX,a),b.INDENT)+c),d+=" else {\n"+c+"}";return d+"\n"},logic_compare$$module$build$src$generators$javascript$logic=function(a,b){const c={EQ:"==",NEQ:"!=",LT:"<",LTE:"<=",GT:">",GTE:">="}[a.getFieldValue("OP")],d=c==="=="||c==="!="?Order$$module$build$src$generators$javascript$javascript_generator.EQUALITY:Order$$module$build$src$generators$javascript$javascript_generator.RELATIONAL,e=b.valueToCode(a,"A",d)||"0";a=b.valueToCode(a,"B",d)||"0";return[e+" "+
c+" "+a,d]},logic_operation$$module$build$src$generators$javascript$logic=function(a,b){const c=a.getFieldValue("OP")==="AND"?"&&":"||",d=c==="&&"?Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_AND:Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_OR;let e=b.valueToCode(a,"A",d);a=b.valueToCode(a,"B",d);e||a?(b=c==="&&"?"true":"false",e||(e=b),a||(a=b)):a=e="false";return[e+" "+c+" "+a,d]},logic_negate$$module$build$src$generators$javascript$logic=
function(a,b){const c=Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_NOT;return["!"+(b.valueToCode(a,"BOOL",c)||"true"),c]},logic_boolean$$module$build$src$generators$javascript$logic=function(a,b){return[a.getFieldValue("BOOL")==="TRUE"?"true":"false",Order$$module$build$src$generators$javascript$javascript_generator.ATOMIC]},logic_null$$module$build$src$generators$javascript$logic=function(a,b){return["null",Order$$module$build$src$generators$javascript$javascript_generator.ATOMIC]},
logic_ternary$$module$build$src$generators$javascript$logic=function(a,b){const c=b.valueToCode(a,"IF",Order$$module$build$src$generators$javascript$javascript_generator.CONDITIONAL)||"false",d=b.valueToCode(a,"THEN",Order$$module$build$src$generators$javascript$javascript_generator.CONDITIONAL)||"null";a=b.valueToCode(a,"ELSE",Order$$module$build$src$generators$javascript$javascript_generator.CONDITIONAL)||"null";return[c+" ? "+d+" : "+a,Order$$module$build$src$generators$javascript$javascript_generator.CONDITIONAL]},
controls_repeat_ext$$module$build$src$generators$javascript$loops=function(a,b){let c;c=a.getField("TIMES")?String(Number(a.getFieldValue("TIMES"))):b.valueToCode(a,"TIMES",Order$$module$build$src$generators$javascript$javascript_generator.ASSIGNMENT)||"0";let d=b.statementToCode(a,"DO");d=b.addLoopTrap(d,a);a="";const e=b.nameDB_.getDistinctName("count",$.NameType$$module$build$src$core$names.VARIABLE);let f=c;c.match(/^\w+$/)||$.isNumber$$module$build$src$core$utils$string(c)||(f=b.nameDB_.getDistinctName("repeat_end",
$.NameType$$module$build$src$core$names.VARIABLE),a+="var "+f+" = "+c+";\n");return a+("for (var "+e+" = 0; "+e+" < "+f+"; "+e+"++) {\n"+d+"}\n")},controls_whileUntil$$module$build$src$generators$javascript$loops=function(a,b){const c=a.getFieldValue("MODE")==="UNTIL";let d=b.valueToCode(a,"BOOL",c?Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_NOT:Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"false",e=b.statementToCode(a,"DO");e=b.addLoopTrap(e,
a);c&&(d="!"+d);return"while ("+d+") {\n"+e+"}\n"},controls_for$$module$build$src$generators$javascript$loops=function(a,b){var c=b.getVariableName(a.getFieldValue("VAR")),d=b.valueToCode(a,"FROM",Order$$module$build$src$generators$javascript$javascript_generator.ASSIGNMENT)||"0",e=b.valueToCode(a,"TO",Order$$module$build$src$generators$javascript$javascript_generator.ASSIGNMENT)||"0";const f=b.valueToCode(a,"BY",Order$$module$build$src$generators$javascript$javascript_generator.ASSIGNMENT)||"1";
let g=b.statementToCode(a,"DO");g=b.addLoopTrap(g,a);if($.isNumber$$module$build$src$core$utils$string(d)&&$.isNumber$$module$build$src$core$utils$string(e)&&$.isNumber$$module$build$src$core$utils$string(f))b=Number(d)<=Number(e),a="for ("+c+" = "+d+"; "+c+(b?" <= ":" >= ")+e+"; "+c,c=Math.abs(Number(f)),a=c===1?a+(b?"++":"--"):a+((b?" += ":" -= ")+c),a+=") {\n"+g+"}\n";else{a="";let h=d;d.match(/^\w+$/)||$.isNumber$$module$build$src$core$utils$string(d)||(h=b.nameDB_.getDistinctName(c+"_start",
$.NameType$$module$build$src$core$names.VARIABLE),a+="var "+h+" = "+d+";\n");d=e;e.match(/^\w+$/)||$.isNumber$$module$build$src$core$utils$string(e)||(d=b.nameDB_.getDistinctName(c+"_end",$.NameType$$module$build$src$core$names.VARIABLE),a+="var "+d+" = "+e+";\n");e=b.nameDB_.getDistinctName(c+"_inc",$.NameType$$module$build$src$core$names.VARIABLE);a+="var "+e+" = ";a=$.isNumber$$module$build$src$core$utils$string(f)?a+(Math.abs(Number(f))+";\n"):a+("Math.abs("+f+");\n");a+="if ("+h+" > "+d+") {\n";
a+=b.INDENT+e+" = -"+e+";\n";a=a+"}\nfor ("+(c+" = "+h+"; "+e+" >= 0 ? "+c+" <= "+d+" : "+c+" >= "+d+"; "+c+" += "+e+") {\n"+g+"}\n")}return a},controls_forEach$$module$build$src$generators$javascript$loops=function(a,b){const c=b.getVariableName(a.getFieldValue("VAR"));var d=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.ASSIGNMENT)||"[]";let e=b.statementToCode(a,"DO");e=b.addLoopTrap(e,a);a="";let f=d;d.match(/^\w+$/)||(f=b.nameDB_.getDistinctName(c+"_list",
$.NameType$$module$build$src$core$names.VARIABLE),a+="var "+f+" = "+d+";\n");d=b.nameDB_.getDistinctName(c+"_index",$.NameType$$module$build$src$core$names.VARIABLE);e=b.INDENT+c+" = "+f+"["+d+"];\n"+e;return a+("for (var "+d+" in "+f+") {\n"+e+"}\n")},controls_flow_statements$$module$build$src$generators$javascript$loops=function(a,b){let c="";b.STATEMENT_PREFIX&&(c+=b.injectId(b.STATEMENT_PREFIX,a));b.STATEMENT_SUFFIX&&(c+=b.injectId(b.STATEMENT_SUFFIX,a));if(b.STATEMENT_PREFIX){const d=a.getSurroundLoop();
d&&!d.suppressPrefixSuffix&&(c+=b.injectId(b.STATEMENT_PREFIX,d))}switch(a.getFieldValue("FLOW")){case "BREAK":return c+"break;\n";case "CONTINUE":return c+"continue;\n"}throw Error("Unknown flow statement.");},math_number$$module$build$src$generators$javascript$math=function(a,b){a=Number(a.getFieldValue("NUM"));return[String(a),a>=0?Order$$module$build$src$generators$javascript$javascript_generator.ATOMIC:Order$$module$build$src$generators$javascript$javascript_generator.UNARY_NEGATION]},math_arithmetic$$module$build$src$generators$javascript$math=
function(a,b){var c={ADD:[" + ",Order$$module$build$src$generators$javascript$javascript_generator.ADDITION],MINUS:[" - ",Order$$module$build$src$generators$javascript$javascript_generator.SUBTRACTION],MULTIPLY:[" * ",Order$$module$build$src$generators$javascript$javascript_generator.MULTIPLICATION],DIVIDE:[" / ",Order$$module$build$src$generators$javascript$javascript_generator.DIVISION],POWER:[null,Order$$module$build$src$generators$javascript$javascript_generator.NONE]}[a.getFieldValue("OP")];
const d=c[0];c=c[1];const e=b.valueToCode(a,"A",c)||"0";a=b.valueToCode(a,"B",c)||"0";return d?[e+d+a,c]:["Math.pow("+e+", "+a+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},math_single$$module$build$src$generators$javascript$math=function(a,b){const c=a.getFieldValue("OP");let d;if(c==="NEG")return a=b.valueToCode(a,"NUM",Order$$module$build$src$generators$javascript$javascript_generator.UNARY_NEGATION)||"0",a[0]==="-"&&(a=" "+a),["-"+a,Order$$module$build$src$generators$javascript$javascript_generator.UNARY_NEGATION];
a=c==="SIN"||c==="COS"||c==="TAN"?b.valueToCode(a,"NUM",Order$$module$build$src$generators$javascript$javascript_generator.DIVISION)||"0":b.valueToCode(a,"NUM",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"0";switch(c){case "ABS":d="Math.abs("+a+")";break;case "ROOT":d="Math.sqrt("+a+")";break;case "LN":d="Math.log("+a+")";break;case "EXP":d="Math.exp("+a+")";break;case "POW10":d="Math.pow(10,"+a+")";break;case "ROUND":d="Math.round("+a+")";break;case "ROUNDUP":d="Math.ceil("+
a+")";break;case "ROUNDDOWN":d="Math.floor("+a+")";break;case "SIN":d="Math.sin("+a+" / 180 * Math.PI)";break;case "COS":d="Math.cos("+a+" / 180 * Math.PI)";break;case "TAN":d="Math.tan("+a+" / 180 * Math.PI)"}if(d)return[d,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL];switch(c){case "LOG10":d="Math.log("+a+") / Math.log(10)";break;case "ASIN":d="Math.asin("+a+") / Math.PI * 180";break;case "ACOS":d="Math.acos("+a+") / Math.PI * 180";break;case "ATAN":d="Math.atan("+
a+") / Math.PI * 180";break;default:throw Error("Unknown math operator: "+c);}return[d,Order$$module$build$src$generators$javascript$javascript_generator.DIVISION]},math_constant$$module$build$src$generators$javascript$math=function(a,b){return{PI:["Math.PI",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER],E:["Math.E",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER],GOLDEN_RATIO:["(1 + Math.sqrt(5)) / 2",Order$$module$build$src$generators$javascript$javascript_generator.DIVISION],
SQRT2:["Math.SQRT2",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER],SQRT1_2:["Math.SQRT1_2",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER],INFINITY:["Infinity",Order$$module$build$src$generators$javascript$javascript_generator.ATOMIC]}[a.getFieldValue("CONSTANT")]},math_number_property$$module$build$src$generators$javascript$math=function(a,b){var c={EVEN:[" % 2 === 0",Order$$module$build$src$generators$javascript$javascript_generator.MODULUS,
Order$$module$build$src$generators$javascript$javascript_generator.EQUALITY],ODD:[" % 2 === 1",Order$$module$build$src$generators$javascript$javascript_generator.MODULUS,Order$$module$build$src$generators$javascript$javascript_generator.EQUALITY],WHOLE:[" % 1 === 0",Order$$module$build$src$generators$javascript$javascript_generator.MODULUS,Order$$module$build$src$generators$javascript$javascript_generator.EQUALITY],POSITIVE:[" > 0",Order$$module$build$src$generators$javascript$javascript_generator.RELATIONAL,
Order$$module$build$src$generators$javascript$javascript_generator.RELATIONAL],NEGATIVE:[" < 0",Order$$module$build$src$generators$javascript$javascript_generator.RELATIONAL,Order$$module$build$src$generators$javascript$javascript_generator.RELATIONAL],DIVISIBLE_BY:[null,Order$$module$build$src$generators$javascript$javascript_generator.MODULUS,Order$$module$build$src$generators$javascript$javascript_generator.EQUALITY],PRIME:[null,Order$$module$build$src$generators$javascript$javascript_generator.NONE,
Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]};const d=a.getFieldValue("PROPERTY"),[e,f,g]=c[d];c=b.valueToCode(a,"NUMBER_TO_CHECK",f)||"0";d==="PRIME"?a=b.provideFunction_("mathIsPrime",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(n) {
// https://en.wikipedia.org/wiki/Primality_test#Naive_methods
if (n == 2 || n == 3) {
return true;
}
// False if n is NaN, negative, is 1, or not whole.
// And false if n is divisible by 2 or 3.
if (isNaN(n) || n <= 1 || n % 1 !== 0 || n % 2 === 0 || n % 3 === 0) {
return false;
}
// Check all the numbers of form 6k +/- 1, up to sqrt(n).
for (var x = 6; x <= Math.sqrt(n) + 1; x += 6) {
if (n % (x - 1) === 0 || n % (x + 1) === 0) {
return false;
}
}
return true;
}
`)+"("+c+")":d==="DIVISIBLE_BY"?(a=b.valueToCode(a,"DIVISOR",Order$$module$build$src$generators$javascript$javascript_generator.MODULUS)||"0",a=c+" % "+a+" === 0"):a=c+e;return[a,g]},math_change$$module$build$src$generators$javascript$math=function(a,b){const c=b.valueToCode(a,"DELTA",Order$$module$build$src$generators$javascript$javascript_generator.ADDITION)||"0";a=b.getVariableName(a.getFieldValue("VAR"));return a+" = (typeof "+a+" === 'number' ? "+a+" : 0) + "+c+";\n"},math_on_list$$module$build$src$generators$javascript$math=
function(a,b){var c=a.getFieldValue("OP");switch(c){case "SUM":a=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"[]";a+=".reduce(function(x, y) {return x + y;}, 0)";break;case "MIN":a=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"[]";a="Math.min.apply(null, "+a+")";break;case "MAX":a=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"[]";a="Math.max.apply(null, "+
a+")";break;case "AVERAGE":c=b.provideFunction_("mathMean",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(myList) {
return myList.reduce(function(x, y) {return x + y;}, 0) / myList.length;
}
`);a=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"[]";a=c+"("+a+")";break;case "MEDIAN":c=b.provideFunction_("mathMedian",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(myList) {
var localList = myList.filter(function (x) {return typeof x === 'number';});
if (!localList.length) return null;
localList.sort(function(a, b) {return b - a;});
if (localList.length % 2 === 0) {
return (localList[localList.length / 2 - 1] + localList[localList.length / 2]) / 2;
} else {
return localList[(localList.length - 1) / 2];
}
}
`);a=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"[]";a=c+"("+a+")";break;case "MODE":c=b.provideFunction_("mathModes",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(values) {
var modes = [];
var counts = [];
var maxCount = 0;
for (var i = 0; i < values.length; i++) {
var value = values[i];
var found = false;
var thisCount;
for (var j = 0; j < counts.length; j++) {
if (counts[j][0] === value) {
thisCount = ++counts[j][1];
found = true;
break;
}
}
if (!found) {
counts.push([value, 1]);
thisCount = 1;
}
maxCount = Math.max(thisCount, maxCount);
}
for (var j = 0; j < counts.length; j++) {
if (counts[j][1] === maxCount) {
modes.push(counts[j][0]);
}
}
return modes;
}
`);a=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"[]";a=c+"("+a+")";break;case "STD_DEV":c=b.provideFunction_("mathStandardDeviation",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(numbers) {
var n = numbers.length;
if (!n) return null;
var mean = numbers.reduce(function(x, y) {return x + y;}) / n;
var variance = 0;
for (var j = 0; j < n; j++) {
variance += Math.pow(numbers[j] - mean, 2);
}
variance /= n;
return Math.sqrt(variance);
}
`);a=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"[]";a=c+"("+a+")";break;case "RANDOM":c=b.provideFunction_("mathRandomList",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(list) {
var x = Math.floor(Math.random() * list.length);
return list[x];
}
`);a=b.valueToCode(a,"LIST",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"[]";a=c+"("+a+")";break;default:throw Error("Unknown operator: "+c);}return[a,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},math_modulo$$module$build$src$generators$javascript$math=function(a,b){const c=b.valueToCode(a,"DIVIDEND",Order$$module$build$src$generators$javascript$javascript_generator.MODULUS)||"0";a=b.valueToCode(a,"DIVISOR",Order$$module$build$src$generators$javascript$javascript_generator.MODULUS)||
"0";return[c+" % "+a,Order$$module$build$src$generators$javascript$javascript_generator.MODULUS]},math_constrain$$module$build$src$generators$javascript$math=function(a,b){const c=b.valueToCode(a,"VALUE",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"0",d=b.valueToCode(a,"LOW",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"0";a=b.valueToCode(a,"HIGH",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"Infinity";return["Math.min(Math.max("+
c+", "+d+"), "+a+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},math_random_int$$module$build$src$generators$javascript$math=function(a,b){const c=b.valueToCode(a,"FROM",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"0";a=b.valueToCode(a,"TO",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"0";return[b.provideFunction_("mathRandomInt",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(a, b) {
if (a > b) {
// Swap a and b to ensure a is smaller.
var c = a;
a = b;
b = c;
}
return Math.floor(Math.random() * (b - a + 1) + a);
}
`)+"("+c+", "+a+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},math_random_float$$module$build$src$generators$javascript$math=function(a,b){return["Math.random()",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},math_atan2$$module$build$src$generators$javascript$math=function(a,b){const c=b.valueToCode(a,"X",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"0";return["Math.atan2("+(b.valueToCode(a,"Y",
Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"0")+", "+c+") / Math.PI * 180",Order$$module$build$src$generators$javascript$javascript_generator.DIVISION]},procedures_defreturn$$module$build$src$generators$javascript$procedures=function(a,b){const c=b.getProcedureName(a.getFieldValue("NAME"));var d="";b.STATEMENT_PREFIX&&(d+=b.injectId(b.STATEMENT_PREFIX,a));b.STATEMENT_SUFFIX&&(d+=b.injectId(b.STATEMENT_SUFFIX,a));d&&(d=b.prefixLines(d,b.INDENT));let e="";b.INFINITE_LOOP_TRAP&&
(e=b.prefixLines(b.injectId(b.INFINITE_LOOP_TRAP,a),b.INDENT));let f="";a.getInput("STACK")&&(f=b.statementToCode(a,"STACK"));let g="";a.getInput("RETURN")&&(g=b.valueToCode(a,"RETURN",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"");let h="";f&&g&&(h=d);g&&(g=b.INDENT+"return "+g+";\n");const k=[],l=a.getVars();for(let m=0;m<l.length;m++)k[m]=b.getVariableName(l[m]);d="function "+c+"("+k.join(", ")+") {\n"+d+e+f+h+g+"}";d=b.scrub_(a,d);b.definitions_["%"+c]=d;return null},
procedures_callreturn$$module$build$src$generators$javascript$procedures=function(a,b){const c=b.getProcedureName(a.getFieldValue("NAME")),d=[],e=a.getVars();for(let f=0;f<e.length;f++)d[f]=b.valueToCode(a,"ARG"+f,Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"null";return[c+"("+d.join(", ")+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},procedures_callnoreturn$$module$build$src$generators$javascript$procedures=function(a,b){return b.forBlock.procedures_callreturn(a,
b)[0]+";\n"},procedures_ifreturn$$module$build$src$generators$javascript$procedures=function(a,b){let c="if ("+(b.valueToCode(a,"CONDITION",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"false")+") {\n";b.STATEMENT_SUFFIX&&(c+=b.prefixLines(b.injectId(b.STATEMENT_SUFFIX,a),b.INDENT));a.hasReturnValue_?(a=b.valueToCode(a,"VALUE",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"null",c+=b.INDENT+"return "+a+";\n"):c+=b.INDENT+"return;\n";return c+
"}\n"},text$$module$build$src$generators$javascript$text=function(a,b){return[b.quote_(a.getFieldValue("TEXT")),Order$$module$build$src$generators$javascript$javascript_generator.ATOMIC]},text_join$$module$build$src$generators$javascript$text=function(a,b){switch(a.itemCount_){case 0:return["''",Order$$module$build$src$generators$javascript$javascript_generator.ATOMIC];case 1:return a=b.valueToCode(a,"ADD0",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''",forceString$$module$build$src$generators$javascript$text(a);
case 2:var c=b.valueToCode(a,"ADD0",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";a=b.valueToCode(a,"ADD1",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";return[forceString$$module$build$src$generators$javascript$text(c)[0]+" + "+forceString$$module$build$src$generators$javascript$text(a)[0],Order$$module$build$src$generators$javascript$javascript_generator.ADDITION];default:c=Array(a.itemCount_);for(let d=0;d<a.itemCount_;d++)c[d]=
b.valueToCode(a,"ADD"+d,Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";return["["+c.join(",")+"].join('')",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]}},text_append$$module$build$src$generators$javascript$text=function(a,b){const c=b.getVariableName(a.getFieldValue("VAR"));a=b.valueToCode(a,"TEXT",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";return c+" += "+forceString$$module$build$src$generators$javascript$text(a)[0]+
";\n"},text_length$$module$build$src$generators$javascript$text=function(a,b){return[(b.valueToCode(a,"VALUE",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"''")+".length",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER]},text_isEmpty$$module$build$src$generators$javascript$text=function(a,b){return["!"+(b.valueToCode(a,"VALUE",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"''")+".length",Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_NOT]},
text_indexOf$$module$build$src$generators$javascript$text=function(a,b){const c=a.getFieldValue("END")==="FIRST"?"indexOf":"lastIndexOf",d=b.valueToCode(a,"FIND",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";b=(b.valueToCode(a,"VALUE",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"''")+"."+c+"("+d+")";return a.workspace.options.oneBasedIndex?[b+" + 1",Order$$module$build$src$generators$javascript$javascript_generator.ADDITION]:[b,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},
text_charAt$$module$build$src$generators$javascript$text=function(a,b){const c=a.getFieldValue("WHERE")||"FROM_START",d=b.valueToCode(a,"VALUE",c==="RANDOM"?Order$$module$build$src$generators$javascript$javascript_generator.NONE:Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"''";switch(c){case "FIRST":return[d+".charAt(0)",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL];case "LAST":return[d+".slice(-1)",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL];
case "FROM_START":return a=b.getAdjusted(a,"AT"),[d+".charAt("+a+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL];case "FROM_END":return a=b.getAdjusted(a,"AT",1,!0),[d+".slice("+a+").charAt(0)",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL];case "RANDOM":return[b.provideFunction_("textRandomLetter",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(text) {
var x = Math.floor(Math.random() * text.length);
return text[x];
}
`)+"("+d+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]}throw Error("Unhandled option (text_charAt).");},text_getSubstring$$module$build$src$generators$javascript$text=function(a,b){var c={FIRST:"First",LAST:"Last",FROM_START:"FromStart",FROM_END:"FromEnd"};const d=a.getFieldValue("WHERE1"),e=a.getFieldValue("WHERE2");var f=d!=="FROM_END"&&d!=="LAST"&&e!=="FROM_END"&&e!=="LAST",g=b.valueToCode(a,"STRING",f?Order$$module$build$src$generators$javascript$javascript_generator.MEMBER:
Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";if(d==="FIRST"&&e==="LAST")return[g,Order$$module$build$src$generators$javascript$javascript_generator.NONE];if(g.match(/^'?\w+'?$/)||f){switch(d){case "FROM_START":c=b.getAdjusted(a,"AT1");break;case "FROM_END":c=b.getAdjusted(a,"AT1",1,!1,Order$$module$build$src$generators$javascript$javascript_generator.SUBTRACTION);c=g+".length - "+c;break;case "FIRST":c="0";break;default:throw Error("Unhandled option (text_getSubstring).");
}switch(e){case "FROM_START":b=b.getAdjusted(a,"AT2",1);break;case "FROM_END":b=b.getAdjusted(a,"AT2",0,!1,Order$$module$build$src$generators$javascript$javascript_generator.SUBTRACTION);b=g+".length - "+b;break;case "LAST":b=g+".length";break;default:throw Error("Unhandled option (text_getSubstring).");}g=g+".slice("+c+", "+b+")"}else f=b.getAdjusted(a,"AT1"),a=b.getAdjusted(a,"AT2"),g=b.provideFunction_("subsequence"+c[d]+c[e],`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(sequence${d==="FROM_END"||d==="FROM_START"?", at1":""}${e==="FROM_END"||e==="FROM_START"?", at2":""}) {
var start = ${getSubstringIndex$$module$build$src$generators$javascript$text("sequence",d,"at1")};
var end = ${getSubstringIndex$$module$build$src$generators$javascript$text("sequence",e,"at2")} + 1;
return sequence.slice(start, end);
}
`)+"("+g+(d==="FROM_END"||d==="FROM_START"?", "+f:"")+(e==="FROM_END"||e==="FROM_START"?", "+a:"")+")";return[g,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},text_changeCase$$module$build$src$generators$javascript$text=function(a,b){const c={UPPERCASE:".toUpperCase()",LOWERCASE:".toLowerCase()",TITLECASE:null}[a.getFieldValue("CASE")];a=b.valueToCode(a,"TEXT",c?Order$$module$build$src$generators$javascript$javascript_generator.MEMBER:Order$$module$build$src$generators$javascript$javascript_generator.NONE)||
"''";return[c?a+c:b.provideFunction_("textToTitleCase",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(str) {
return str.replace(/\\S+/g,
function(txt) {return txt[0].toUpperCase() + txt.substring(1).toLowerCase();});
}
`)+"("+a+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},text_trim$$module$build$src$generators$javascript$text=function(a,b){const c={LEFT:".replace(/^[\\s\\xa0]+/, '')",RIGHT:".replace(/[\\s\\xa0]+$/, '')",BOTH:".trim()"}[a.getFieldValue("MODE")];return[(b.valueToCode(a,"TEXT",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"''")+c,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},text_print$$module$build$src$generators$javascript$text=
function(a,b){return"window.alert("+(b.valueToCode(a,"TEXT",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''")+");\n"},text_prompt_ext$$module$build$src$generators$javascript$text=function(a,b){b="window.prompt("+(a.getField("TEXT")?b.quote_(a.getFieldValue("TEXT")):b.valueToCode(a,"TEXT",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''")+")";a.getFieldValue("TYPE")==="NUMBER"&&(b="Number("+b+")");return[b,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},
text_count$$module$build$src$generators$javascript$text=function(a,b){const c=b.valueToCode(a,"TEXT",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";a=b.valueToCode(a,"SUB",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";return[b.provideFunction_("textCount",`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle) {
if (needle.length === 0) {
return haystack.length + 1;
} else {
return haystack.split(needle).length - 1;
}
}
`)+"("+c+", "+a+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},text_replace$$module$build$src$generators$javascript$text=function(a,b){const c=b.valueToCode(a,"TEXT",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''",d=b.valueToCode(a,"FROM",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";a=b.valueToCode(a,"TO",Order$$module$build$src$generators$javascript$javascript_generator.NONE)||"''";return[b.provideFunction_("textReplace",
`
function ${b.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle, replacement) {
needle = needle.replace(/([-()\\[\\]{}+?*.$\\^|,:#<!\\\\])/g, '\\\\$1')
.replace(/\\x08/g, '\\\\x08');
return haystack.replace(new RegExp(needle, 'g'), replacement);
}
`)+"("+c+", "+d+", "+a+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},text_reverse$$module$build$src$generators$javascript$text=function(a,b){return[(b.valueToCode(a,"TEXT",Order$$module$build$src$generators$javascript$javascript_generator.MEMBER)||"''")+".split('').reverse().join('')",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},variables_get$$module$build$src$generators$javascript$variables=function(a,b){return[b.getVariableName(a.getFieldValue("VAR")),
Order$$module$build$src$generators$javascript$javascript_generator.ATOMIC]},variables_set$$module$build$src$generators$javascript$variables=function(a,b){const c=b.valueToCode(a,"VALUE",Order$$module$build$src$generators$javascript$javascript_generator.ASSIGNMENT)||"0";return b.getVariableName(a.getFieldValue("VAR"))+" = "+c+";\n"},Order$$module$build$src$generators$javascript$javascript_generator;
(function(a){a[a.ATOMIC=0]="ATOMIC";a[a.NEW=1.1]="NEW";a[a.MEMBER=1.2]="MEMBER";a[a.FUNCTION_CALL=2]="FUNCTION_CALL";a[a.INCREMENT=3]="INCREMENT";a[a.DECREMENT=3]="DECREMENT";a[a.BITWISE_NOT=4.1]="BITWISE_NOT";a[a.UNARY_PLUS=4.2]="UNARY_PLUS";a[a.UNARY_NEGATION=4.3]="UNARY_NEGATION";a[a.LOGICAL_NOT=4.4]="LOGICAL_NOT";a[a.TYPEOF=4.5]="TYPEOF";a[a.VOID=4.6]="VOID";a[a.DELETE=4.7]="DELETE";a[a.AWAIT=4.8]="AWAIT";a[a.EXPONENTIATION=5]="EXPONENTIATION";a[a.MULTIPLICATION=5.1]="MULTIPLICATION";a[a.DIVISION=
5.2]="DIVISION";a[a.MODULUS=5.3]="MODULUS";a[a.SUBTRACTION=6.1]="SUBTRACTION";a[a.ADDITION=6.2]="ADDITION";a[a.BITWISE_SHIFT=7]="BITWISE_SHIFT";a[a.RELATIONAL=8]="RELATIONAL";a[a.IN=8]="IN";a[a.INSTANCEOF=8]="INSTANCEOF";a[a.EQUALITY=9]="EQUALITY";a[a.BITWISE_AND=10]="BITWISE_AND";a[a.BITWISE_XOR=11]="BITWISE_XOR";a[a.BITWISE_OR=12]="BITWISE_OR";a[a.LOGICAL_AND=13]="LOGICAL_AND";a[a.LOGICAL_OR=14]="LOGICAL_OR";a[a.CONDITIONAL=15]="CONDITIONAL";a[a.ASSIGNMENT=16]="ASSIGNMENT";a[a.YIELD=17]="YIELD";
a[a.COMMA=18]="COMMA";a[a.NONE=99]="NONE"})(Order$$module$build$src$generators$javascript$javascript_generator||(Order$$module$build$src$generators$javascript$javascript_generator={}));
var JavascriptGenerator$$module$build$src$generators$javascript$javascript_generator=class extends $.CodeGenerator$$module$build$src$core$generator{constructor(a="JavaScript"){super(a);this.ORDER_OVERRIDES=[[Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL,Order$$module$build$src$generators$javascript$javascript_generator.MEMBER],[Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL],
[Order$$module$build$src$generators$javascript$javascript_generator.MEMBER,Order$$module$build$src$generators$javascript$javascript_generator.MEMBER],[Order$$module$build$src$generators$javascript$javascript_generator.MEMBER,Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL],[Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_NOT,Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_NOT],[Order$$module$build$src$generators$javascript$javascript_generator.MULTIPLICATION,
Order$$module$build$src$generators$javascript$javascript_generator.MULTIPLICATION],[Order$$module$build$src$generators$javascript$javascript_generator.ADDITION,Order$$module$build$src$generators$javascript$javascript_generator.ADDITION],[Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_AND,Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_AND],[Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_OR,Order$$module$build$src$generators$javascript$javascript_generator.LOGICAL_OR]];
this.isInitialized=!1;for(const b in Order$$module$build$src$generators$javascript$javascript_generator)a=Order$$module$build$src$generators$javascript$javascript_generator[b],typeof a!=="string"&&(this["ORDER_"+b]=a);this.addReservedWords("break,case,catch,class,const,continue,debugger,default,delete,do,else,export,extends,finally,for,function,if,import,in,instanceof,new,return,super,switch,this,throw,try,typeof,var,void,while,with,yield,enum,implements,interface,let,package,private,protected,public,static,await,null,true,false,arguments,"+
Object.getOwnPropertyNames(globalThis).join(","))}init(a){super.init(a);this.nameDB_?this.nameDB_.reset():this.nameDB_=new $.Names$$module$build$src$core$names(this.RESERVED_WORDS_);this.nameDB_.setVariableMap(a.getVariableMap());this.nameDB_.populateVariables(a);this.nameDB_.populateProcedures(a);const b=[];var c=$.allDeveloperVariables$$module$build$src$core$variables(a);for(let d=0;d<c.length;d++)b.push(this.nameDB_.getName(c[d],$.NameType$$module$build$src$core$names.DEVELOPER_VARIABLE));a=$.allUsedVarModels$$module$build$src$core$variables(a);
for(c=0;c<a.length;c++)b.push(this.nameDB_.getName(a[c].getId(),$.NameType$$module$build$src$core$names.VARIABLE));b.length&&(this.definitions_.variables="var "+b.join(", ")+";");this.isInitialized=!0}finish(a){const b=Object.values(this.definitions_);super.finish(a);this.isInitialized=!1;this.nameDB_.reset();return b.join("\n\n")+"\n\n\n"+a}scrubNakedValue(a){return a+";\n"}quote_(a){a=a.replace(/\\/g,"\\\\").replace(/\n/g,"\\\n").replace(/'/g,"\\'");return"'"+a+"'"}multiline_quote_(a){return a.split(/\n/g).map(this.quote_).join(" + '\\n' +\n")}scrub_(a,
b,c=!1){let d="";if(!a.outputConnection||!a.outputConnection.targetConnection){var e=a.getCommentText();e&&(e=$.wrap$$module$build$src$core$utils$string(e,this.COMMENT_WRAP-3),d+=this.prefixLines(e+"\n","// "));for(let f=0;f<a.inputList.length;f++)a.inputList[f].type===$.inputTypes$$module$build$src$core$inputs$input_types.VALUE&&(e=a.inputList[f].connection.targetBlock())&&(e=this.allNestedComments(e))&&(d+=this.prefixLines(e,"// "))}a=a.nextConnection&&a.nextConnection.targetBlock();c=c?"":this.blockToCode(a);
return d+b+c}getAdjusted(a,b,c=0,d=!1,e=Order$$module$build$src$generators$javascript$javascript_generator.NONE){a.workspace.options.oneBasedIndex&&c--;const f=a.workspace.options.oneBasedIndex?"1":"0";let g=e;c>0?g=Order$$module$build$src$generators$javascript$javascript_generator.ADDITION:c<0?g=Order$$module$build$src$generators$javascript$javascript_generator.SUBTRACTION:d&&(g=Order$$module$build$src$generators$javascript$javascript_generator.UNARY_NEGATION);a=this.valueToCode(a,b,g)||f;if(c===
0&&!d)return a;if($.isNumber$$module$build$src$core$utils$string(a))return a=String(Number(a)+c),d&&(a=String(-Number(a))),a;c>0?a=`${a} + ${c}`:c<0&&(a=`${a} - ${-c}`);d&&(a=c?`-(${a})`:`-${a}`);Math.floor(e)>=Math.floor(g)&&(a=`(${a})`);return a}},module$build$src$generators$javascript$javascript_generator={};module$build$src$generators$javascript$javascript_generator.JavascriptGenerator=JavascriptGenerator$$module$build$src$generators$javascript$javascript_generator;
module$build$src$generators$javascript$javascript_generator.Order=Order$$module$build$src$generators$javascript$javascript_generator;var getSubstringIndex$$module$build$src$generators$javascript$lists=function(a,b,c){return b==="FIRST"?"0":b==="FROM_END"?a+".length - 1 - "+c:b==="LAST"?a+".length - 1":c},module$build$src$generators$javascript$lists={};module$build$src$generators$javascript$lists.lists_create_empty=lists_create_empty$$module$build$src$generators$javascript$lists;module$build$src$generators$javascript$lists.lists_create_with=lists_create_with$$module$build$src$generators$javascript$lists;
module$build$src$generators$javascript$lists.lists_getIndex=lists_getIndex$$module$build$src$generators$javascript$lists;module$build$src$generators$javascript$lists.lists_getSublist=lists_getSublist$$module$build$src$generators$javascript$lists;module$build$src$generators$javascript$lists.lists_indexOf=lists_indexOf$$module$build$src$generators$javascript$lists;module$build$src$generators$javascript$lists.lists_isEmpty=lists_isEmpty$$module$build$src$generators$javascript$lists;
module$build$src$generators$javascript$lists.lists_length=lists_length$$module$build$src$generators$javascript$lists;module$build$src$generators$javascript$lists.lists_repeat=lists_repeat$$module$build$src$generators$javascript$lists;module$build$src$generators$javascript$lists.lists_reverse=lists_reverse$$module$build$src$generators$javascript$lists;module$build$src$generators$javascript$lists.lists_setIndex=lists_setIndex$$module$build$src$generators$javascript$lists;
module$build$src$generators$javascript$lists.lists_sort=lists_sort$$module$build$src$generators$javascript$lists;module$build$src$generators$javascript$lists.lists_split=lists_split$$module$build$src$generators$javascript$lists;var controls_ifelse$$module$build$src$generators$javascript$logic=controls_if$$module$build$src$generators$javascript$logic,module$build$src$generators$javascript$logic={};module$build$src$generators$javascript$logic.controls_if=controls_if$$module$build$src$generators$javascript$logic;module$build$src$generators$javascript$logic.controls_ifelse=controls_if$$module$build$src$generators$javascript$logic;module$build$src$generators$javascript$logic.logic_boolean=logic_boolean$$module$build$src$generators$javascript$logic;
module$build$src$generators$javascript$logic.logic_compare=logic_compare$$module$build$src$generators$javascript$logic;module$build$src$generators$javascript$logic.logic_negate=logic_negate$$module$build$src$generators$javascript$logic;module$build$src$generators$javascript$logic.logic_null=logic_null$$module$build$src$generators$javascript$logic;module$build$src$generators$javascript$logic.logic_operation=logic_operation$$module$build$src$generators$javascript$logic;
module$build$src$generators$javascript$logic.logic_ternary=logic_ternary$$module$build$src$generators$javascript$logic;var controls_repeat$$module$build$src$generators$javascript$loops=controls_repeat_ext$$module$build$src$generators$javascript$loops,module$build$src$generators$javascript$loops={};module$build$src$generators$javascript$loops.controls_flow_statements=controls_flow_statements$$module$build$src$generators$javascript$loops;module$build$src$generators$javascript$loops.controls_for=controls_for$$module$build$src$generators$javascript$loops;module$build$src$generators$javascript$loops.controls_forEach=controls_forEach$$module$build$src$generators$javascript$loops;
module$build$src$generators$javascript$loops.controls_repeat=controls_repeat_ext$$module$build$src$generators$javascript$loops;module$build$src$generators$javascript$loops.controls_repeat_ext=controls_repeat_ext$$module$build$src$generators$javascript$loops;module$build$src$generators$javascript$loops.controls_whileUntil=controls_whileUntil$$module$build$src$generators$javascript$loops;var math_round$$module$build$src$generators$javascript$math=math_single$$module$build$src$generators$javascript$math,math_trig$$module$build$src$generators$javascript$math=math_single$$module$build$src$generators$javascript$math,module$build$src$generators$javascript$math={};module$build$src$generators$javascript$math.math_arithmetic=math_arithmetic$$module$build$src$generators$javascript$math;module$build$src$generators$javascript$math.math_atan2=math_atan2$$module$build$src$generators$javascript$math;
module$build$src$generators$javascript$math.math_change=math_change$$module$build$src$generators$javascript$math;module$build$src$generators$javascript$math.math_constant=math_constant$$module$build$src$generators$javascript$math;module$build$src$generators$javascript$math.math_constrain=math_constrain$$module$build$src$generators$javascript$math;module$build$src$generators$javascript$math.math_modulo=math_modulo$$module$build$src$generators$javascript$math;
module$build$src$generators$javascript$math.math_number=math_number$$module$build$src$generators$javascript$math;module$build$src$generators$javascript$math.math_number_property=math_number_property$$module$build$src$generators$javascript$math;module$build$src$generators$javascript$math.math_on_list=math_on_list$$module$build$src$generators$javascript$math;module$build$src$generators$javascript$math.math_random_float=math_random_float$$module$build$src$generators$javascript$math;
module$build$src$generators$javascript$math.math_random_int=math_random_int$$module$build$src$generators$javascript$math;module$build$src$generators$javascript$math.math_round=math_single$$module$build$src$generators$javascript$math;module$build$src$generators$javascript$math.math_single=math_single$$module$build$src$generators$javascript$math;module$build$src$generators$javascript$math.math_trig=math_single$$module$build$src$generators$javascript$math;var procedures_defnoreturn$$module$build$src$generators$javascript$procedures=procedures_defreturn$$module$build$src$generators$javascript$procedures,module$build$src$generators$javascript$procedures={};module$build$src$generators$javascript$procedures.procedures_callnoreturn=procedures_callnoreturn$$module$build$src$generators$javascript$procedures;module$build$src$generators$javascript$procedures.procedures_callreturn=procedures_callreturn$$module$build$src$generators$javascript$procedures;
module$build$src$generators$javascript$procedures.procedures_defnoreturn=procedures_defreturn$$module$build$src$generators$javascript$procedures;module$build$src$generators$javascript$procedures.procedures_defreturn=procedures_defreturn$$module$build$src$generators$javascript$procedures;module$build$src$generators$javascript$procedures.procedures_ifreturn=procedures_ifreturn$$module$build$src$generators$javascript$procedures;var strRegExp$$module$build$src$generators$javascript$text=/^\s*'([^']|\\')*'\s*$/,forceString$$module$build$src$generators$javascript$text=function(a){return strRegExp$$module$build$src$generators$javascript$text.test(a)?[a,Order$$module$build$src$generators$javascript$javascript_generator.ATOMIC]:["String("+a+")",Order$$module$build$src$generators$javascript$javascript_generator.FUNCTION_CALL]},getSubstringIndex$$module$build$src$generators$javascript$text=function(a,b,c){return b==="FIRST"?"0":
b==="FROM_END"?a+".length - 1 - "+c:b==="LAST"?a+".length - 1":c},text_prompt$$module$build$src$generators$javascript$text=text_prompt_ext$$module$build$src$generators$javascript$text,module$build$src$generators$javascript$text={};module$build$src$generators$javascript$text.text=text$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_append=text_append$$module$build$src$generators$javascript$text;
module$build$src$generators$javascript$text.text_changeCase=text_changeCase$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_charAt=text_charAt$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_count=text_count$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_getSubstring=text_getSubstring$$module$build$src$generators$javascript$text;
module$build$src$generators$javascript$text.text_indexOf=text_indexOf$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_isEmpty=text_isEmpty$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_join=text_join$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_length=text_length$$module$build$src$generators$javascript$text;
module$build$src$generators$javascript$text.text_print=text_print$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_prompt=text_prompt_ext$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_prompt_ext=text_prompt_ext$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_replace=text_replace$$module$build$src$generators$javascript$text;
module$build$src$generators$javascript$text.text_reverse=text_reverse$$module$build$src$generators$javascript$text;module$build$src$generators$javascript$text.text_trim=text_trim$$module$build$src$generators$javascript$text;var module$build$src$generators$javascript$variables={};module$build$src$generators$javascript$variables.variables_get=variables_get$$module$build$src$generators$javascript$variables;module$build$src$generators$javascript$variables.variables_set=variables_set$$module$build$src$generators$javascript$variables;var module$build$src$generators$javascript$variables_dynamic={};module$build$src$generators$javascript$variables_dynamic.variables_get_dynamic=variables_get$$module$build$src$generators$javascript$variables;module$build$src$generators$javascript$variables_dynamic.variables_set_dynamic=variables_set$$module$build$src$generators$javascript$variables;var javascriptGenerator$$module$build$src$generators$javascript=new JavascriptGenerator$$module$build$src$generators$javascript$javascript_generator,generators$$module$build$src$generators$javascript=Object.assign({},module$build$src$generators$javascript$lists,module$build$src$generators$javascript$logic,module$build$src$generators$javascript$loops,module$build$src$generators$javascript$math,module$build$src$generators$javascript$procedures,module$build$src$generators$javascript$text,module$build$src$generators$javascript$variables,
module$build$src$generators$javascript$variables_dynamic);for(const a in generators$$module$build$src$generators$javascript)javascriptGenerator$$module$build$src$generators$javascript.forBlock[a]=generators$$module$build$src$generators$javascript[a];var module$build$src$generators$javascript={};module$build$src$generators$javascript.JavascriptGenerator=JavascriptGenerator$$module$build$src$generators$javascript$javascript_generator;module$build$src$generators$javascript.Order=Order$$module$build$src$generators$javascript$javascript_generator;
module$build$src$generators$javascript.javascriptGenerator=javascriptGenerator$$module$build$src$generators$javascript;
module$build$src$generators$javascript.__namespace__=$;
return module$build$src$generators$javascript;
}));
//# sourceMappingURL=javascript_compressed.js.map

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

395
crumbblocks/painter.html Normal file
View File

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

View File

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

View File

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

View File

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

31
crumbblocks/readme.txt Normal file
View File

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

View File

@@ -0,0 +1,687 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>☀️ Solar Wasserkocher</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="lib/blockly/blockly.min.js"></script>
<script src="lib/blockly/javascript.min.js"></script>
<script src="lib/blockly/de.js"></script>
<style>
:root {
--bg: #05140a;
/* Deep Forest Black */
--panel: #0d2615;
/* Dark Moss */
--txt: #e0f2f1;
/* Mint White */
--accent: #00e676;
/* Vibrant Mint */
--accent-hover: #b9f6ca;
--border: #1b5e20;
}
body {
background: var(--bg);
color: var(--txt);
font-family: 'Segoe UI', sans-serif;
height: 100vh;
margin: 0;
display: flex;
flex-direction: column;
}
header {
padding: 15px 20px;
background: rgba(13, 38, 21, 0.95);
border-bottom: 2px solid var(--accent);
display: flex;
align-items: center;
gap: 15px;
box-shadow: 0 4px 20px rgba(0, 230, 118, 0.1);
}
h2 {
margin: 0;
flex: 1;
font-size: 1.2rem;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--accent);
}
button {
padding: 10px 20px;
border-radius: 20px;
border: 1px solid var(--accent);
font-weight: bold;
cursor: pointer;
background: transparent;
color: var(--accent);
transition: all 0.2s;
}
button:hover {
background: rgba(0, 230, 118, 0.1);
transform: translateY(-2px);
box-shadow: 0 2px 10px rgba(0, 230, 118, 0.2);
}
button.primary {
background: var(--accent);
color: #003d15;
border: none;
}
button.primary:hover {
background: var(--accent-hover);
color: #000;
box-shadow: 0 0 15px var(--accent);
}
button.action {
border-color: #ff9800;
color: #ff9800;
}
button.action:hover {
background: rgba(255, 152, 0, 0.1);
box-shadow: 0 2px 10px rgba(255, 152, 0, 0.2);
}
#main {
flex: 1;
display: flex;
overflow: hidden;
}
#blocklyDiv {
flex: 3;
border-right: 1px solid var(--border);
}
#simDiv {
flex: 2;
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
background: var(--panel);
position: relative;
}
/* Visualization */
#vis {
width: 100%;
height: 300px;
background: linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 100%);
border-radius: 10px;
position: relative;
overflow: hidden;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2);
}
/* Elements */
.sun {
position: absolute;
top: 20px;
right: 20px;
width: 60px;
height: 60px;
background: #FFD700;
border-radius: 50%;
box-shadow: 0 0 40px #FFD700;
transition: all 0.5s;
}
.cloud {
position: absolute;
top: 40px;
left: -100px;
width: 120px;
height: 60px;
background: rgba(255, 255, 255, 0.8);
border-radius: 30px;
transition: left 2s linear;
filter: blur(5px);
}
.kettle-base {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 120px;
height: 160px;
background: rgba(255, 255, 255, 0.3);
border: 2px solid #555;
border-radius: 10px 10px 20px 20px;
backdrop-filter: blur(2px);
z-index: 10;
}
.water {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 20%;
background: #2196F3;
opacity: 0.8;
border-radius: 0 0 18px 18px;
transition: height 0.5s, background-color 0.5s;
}
.bubbles {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
display: none;
}
.bubble {
position: absolute;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
animation: rise 2s infinite;
}
@keyframes rise {
0% {
bottom: 0;
opacity: 0;
}
50% {
opacity: 1;
}
100% {
bottom: 100%;
opacity: 0;
}
}
.panel {
position: absolute;
bottom: 20px;
left: 20px;
width: 60px;
height: 80px;
background: #111;
border: 2px solid #666;
transform: skewX(-10deg);
}
.wire {
position: absolute;
bottom: 20px;
left: 80px;
width: 80px;
height: 5px;
background: #333;
transform: rotate(-5deg);
z-index: 5;
}
.info-overlay {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 10px;
border-radius: 5px;
font-family: monospace;
}
#log {
flex: 1;
background: #111;
color: #0f0;
font-family: monospace;
padding: 10px;
overflow-y: auto;
border-radius: 5px;
font-size: 0.9rem;
}
.heating .kettle-base {
box-shadow: 0 0 20px red;
border-color: red;
}
/* === BLOCKLY DEEP FOREST OVERRIDES === */
.blocklySvg {
background-color: var(--bg) !important;
}
/* Toolbox (Sidebar) - Dark Grey High Contrast */
.blocklyToolboxDiv {
background-color: #121212 !important;
color: #eeeeee !important;
border-right: 1px solid var(--border);
}
/* Category Labels */
.blocklyTreeLabel {
font-family: 'Segoe UI', sans-serif !important;
color: #bbbbbb !important;
}
/* Rows */
.blocklyTreeRow {
border-left: 4px solid transparent;
margin-bottom: 2px !important;
line-height: 24px !important;
}
/* Hover */
.blocklyTreeRow:hover {
background-color: #1e1e1e !important;
border-left-color: var(--accent-hover);
}
/* Selected */
.blocklyTreeSelected .blocklyTreeRow {
background-color: #0d2615 !important;
/* Moss background for active */
border-left-color: var(--accent) !important;
}
.blocklyTreeSelected .blocklyTreeLabel {
color: var(--accent) !important;
font-weight: bold;
}
/* Flyout (Popup) */
.blocklyFlyoutBackground {
fill: #181818 !important;
fill-opacity: 0.98 !important;
}
/* Text in Flyout headers */
.blocklyFlyoutLabelText {
fill: var(--txt) !important;
}
/* Scrollbars */
.blocklyScrollbarHandle {
fill: var(--accent) !important;
fill-opacity: 0.5 !important;
}
/* === DROPDOWNS & MENUS (The "Helle Overlay" Fix) === */
.blocklyDropDownDiv,
.blocklyWidgetDiv .goog-menu,
.goog-menu {
background-color: #121212 !important;
border: 1px solid var(--accent) !important;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.8) !important;
border-radius: 4px !important;
}
/* Content Text in Menus */
.goog-menuitem-content,
.goog-menuitem,
.blocklyDropDownDiv .goog-menuitem-content {
color: #eeeeee !important;
background-color: transparent !important;
font-family: 'Segoe UI', sans-serif !important;
}
/* Hover State in Menus */
.goog-menuitem-highlight,
.goog-menuitem:hover {
background-color: rgba(0, 230, 118, 0.2) !important;
border-color: transparent !important;
}
/* Input Fields (Number editing) */
.blocklyHtmlInput {
background-color: #05140a !important;
color: var(--accent) !important;
border: 1px solid var(--accent) !important;
border-radius: 4px !important;
font-family: monospace !important;
padding: 2px 5px !important;
}
/* Tooltips */
.blocklyTooltipDiv {
background-color: #0d2615 !important;
color: var(--txt) !important;
border: 1px solid var(--accent) !important;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5) !important;
}
/* === FINAL UI POLISH === */
#main {
border: 2px solid #4fc3f7 !important;
/* Light Blue Accent */
box-shadow: 0 0 15px rgba(79, 195, 247, 0.1);
}
/* Force Toolbox Background deeper */
.blocklyToolboxContents,
.blocklyTreeRoot,
.blocklyToolbox {
background-color: #121212 !important;
}
</style>
</head>
<body>
<header>
<h2>☀️ Solar-Wasserkocher Sim</h2>
<span id="status" style="font-size: 0.9rem; color: #aaa;">Bereit.</span>
<button onclick="runCode()" class="primary">▶️ Starten</button>
<button onclick="resetSim()">🔄 Reset</button>
<button onclick="exportData()" class="action">🚀 An Crew senden</button>
</header>
<div id="main">
<div id="blocklyDiv"></div>
<div id="simDiv">
<div id="vis">
<div class="sun" id="sun"></div>
<div class="cloud" id="cloud"></div>
<div class="panel"></div>
<div class="wire"></div>
<div class="kettle-base" id="kettle">
<div class="water" id="water"></div>
<div class="bubbles" id="bubbles"></div>
</div>
<div class="info-overlay">
Temp: <span id="valTemp">20.0</span> °C<br>
Power: <span id="valPower">0</span> W<br>
Energie: <span id="valEnergy">0</span> kJ<br>
Zeit: <span id="valTime">0</span> s
</div>
</div>
<div id="log">Logs:<br></div>
</div>
</div>
<xml id="toolbox" style="display: none">
<category name="Sensoren" colour="180">
<block type="sensor_temp"></block>
<block type="sensor_power"></block>
</category>
<category name="Aktor" colour="30">
<block type="heater_switch"></block>
<block type="wait_seconds"></block>
</category>
<category name="Logik" colour="120">
<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="150">
<block type="controls_whileUntil"></block>
</category>
<category name="Mathe" colour="200">
<block type="math_number"></block>
<block type="math_arithmetic"></block>
</category>
<category name="Variablen" custom="VARIABLE" colour="90"></category>
</xml>
<script>
// === PHYSICS ENGINE ===
const SIM = {
running: false,
time: 0,
dt: 0.1, // 100ms steps
cloudPos: -100,
// State
temp: 20.0,
volume: 1.0, // Liter (kg)
heaterOn: false,
energyUsed: 0.0, // kJ
// Constants
c: 4.184, // kJ/(kg*K) specific heat water
maxPower: 2000, // Watt at full sun
lossFactor: 0.05, // Cooling per degree delta T
// External
sunIntensity: 1.0, // 0..1
reset() {
this.running = false;
this.time = 0;
this.temp = 20.0;
this.heaterOn = false;
this.energyUsed = 0.0;
this.sunIntensity = 1.0;
this.cloudPos = -100;
updateVis();
log("Sim reset.");
},
step() {
if (!this.running) return;
// Environment logic
this.time += this.dt;
this.cloudPos += 0.5;
if (this.cloudPos > 400) this.cloudPos = -150;
// Sun logic (cloud blocks sun)
const cloudCenter = this.cloudPos + 60;
const panelCenter = 40; // Approx css left
let dist = Math.abs(cloudCenter - panelCenter);
this.sunIntensity = (dist < 80) ? 0.2 : 1.0;
// Physics
const currentPowerW = this.heaterOn ? (this.maxPower * this.sunIntensity) : 0;
const heatingKJ = (currentPowerW / 1000) * this.dt;
const deltaT_heat = heatingKJ / (this.volume * this.c);
const deltaT_cool = (this.temp - 20) * this.lossFactor * this.dt;
this.temp += (deltaT_heat - deltaT_cool);
this.energyUsed += heatingKJ;
if (this.temp > 100) this.temp = 100; // Cap at boiling (energy wasted phase) -> actually lets cap valid boiling, but track waste?
// Let's cap at 120 for "Explosion" visualization? kept simple for now.
updateVis();
}
};
function updateVis() {
document.getElementById('valTemp').innerText = SIM.temp.toFixed(1);
document.getElementById('valPower').innerText = SIM.heaterOn ? (SIM.maxPower * SIM.sunIntensity).toFixed(0) : "0";
document.getElementById('valEnergy').innerText = SIM.energyUsed.toFixed(1);
document.getElementById('valTime').innerText = SIM.time.toFixed(1);
// Kettle
const h = Math.min(SIM.temp, 100);
document.getElementById('water').style.height = (20 + (h / 100) * 60) + "%"; // Mock level
// Color: Blue -> Red
const r = Math.min(255, (SIM.temp - 20) * 3);
const b = Math.max(0, 255 - (SIM.temp - 20) * 3);
document.getElementById('water').style.backgroundColor = `rgb(${r}, 0, ${b})`;
// Bubbles
document.getElementById('bubbles').style.display = (SIM.temp > 95) ? 'block' : 'none';
// Glow
const kettle = document.getElementById('kettle');
if (SIM.heaterOn) kettle.classList.add('heating'); else kettle.classList.remove('heating');
// Cloud
document.getElementById('cloud').style.left = SIM.cloudPos + "px";
// Sun dim
document.getElementById('sun').style.opacity = SIM.sunIntensity;
}
function log(msg) {
const l = document.getElementById('log');
l.innerHTML += `> ${msg}<br>`;
l.scrollTop = l.scrollHeight;
}
// === BLOCKLY SETUP ===
Blockly.defineBlocksWithJsonArray([
{
"type": "sensor_temp",
"message0": "🌡️ Temperatur (°C)",
"output": "Number",
"colour": 230,
"tooltip": "Misst die Wassertemperatur"
},
{
"type": "sensor_power",
"message0": "☀️ Verfügbare Power (W)",
"output": "Number",
"colour": 230,
"tooltip": "Zeigt wie viel Sonnenenergie da ist"
},
{
"type": "heater_switch",
"message0": "🔥 Heizung %1",
"args0": [
{
"type": "field_dropdown",
"name": "STATE",
"options": [["AN", "ON"], ["AUS", "OFF"]]
}
],
"previousStatement": null,
"nextStatement": null,
"colour": 160,
"tooltip": "Schaltet den Heizstab"
},
{
"type": "wait_seconds",
"message0": "⏳ Warte %1 Sek.",
"args0": [
{
"type": "field_number",
"name": "SECONDS",
"value": 1,
"min": 0.1
}
],
"previousStatement": null,
"nextStatement": null,
"colour": 160,
"tooltip": "Wartet und lässt Zeit vergehen"
}
]);
const workspace = Blockly.inject('blocklyDiv', {
toolbox: document.getElementById('toolbox'),
theme: Blockly.Themes.Dark,
renderer: 'zelos'
});
// Code Generators
javascript.javascriptGenerator.forBlock['sensor_temp'] = function (block) {
return ['getTemp()', javascript.Order.ATOMIC];
};
javascript.javascriptGenerator.forBlock['sensor_power'] = function (block) {
return ['getPower()', javascript.Order.ATOMIC];
};
javascript.javascriptGenerator.forBlock['heater_switch'] = function (block) {
var state = block.getFieldValue('STATE');
return 'setHeater("' + state + '");\n';
};
javascript.javascriptGenerator.forBlock['wait_seconds'] = function (block) {
var sec = block.getFieldValue('SECONDS');
return 'await wait(' + sec + ');\n';
};
// === INTERFACE FOR GENERATED CODE ===
function getTemp() { return SIM.temp; }
function getPower() { return SIM.sunIntensity * SIM.maxPower; }
function setHeater(state) {
SIM.heaterOn = (state === 'ON');
log(`Heizung: ${state}`);
updateVis();
}
function wait(sec) {
return new Promise(resolve => setTimeout(resolve, sec * 1000));
}
// Safety Stepper
async function _step(id) {
workspace.highlightBlock(id);
await new Promise(r => setTimeout(r, 10)); // Minimal delay to keep UI alive
}
let loopInterval;
async function runCode() {
resetSim();
SIM.running = true;
document.getElementById('status').innerText = "Läuft...";
// Start Physics Loop independent of Code
if (loopInterval) clearInterval(loopInterval);
loopInterval = setInterval(() => SIM.step(), SIM.dt * 1000); // 100ms real time = 0.1s sim time
// Generator Config
javascript.javascriptGenerator.STATEMENT_PREFIX = 'await _step(%1);\n';
javascript.javascriptGenerator.addReservedWords('code');
var code = javascript.javascriptGenerator.workspaceToCode(workspace);
try {
// Wrap in async function
const wrappedCode = `(async () => {
${code}
log("🏁 Programm Ende");
SIM.running = false;
clearInterval(loopInterval);
document.getElementById('status').innerText = "Fertig.";
})();`;
eval(wrappedCode);
} catch (e) {
log("❌ Fehler: " + e);
SIM.running = false;
}
}
function resetSim() {
SIM.reset();
if (loopInterval) clearInterval(loopInterval);
document.getElementById('log').innerHTML = "Logs:<br>";
document.getElementById('status').innerText = "Reset.";
workspace.highlightBlock(null);
}
async function exportData() {
const data = {
mission: "solar_kettle",
energy_kj: SIM.energyUsed.toFixed(2),
final_temp: SIM.temp.toFixed(1),
sim_time: SIM.time.toFixed(1),
code_summary: "Blockly Code ausgeführt", // Could be more creating
timestamp: new Date().toISOString()
};
const json = JSON.stringify(data, null, 2);
try {
await navigator.clipboard.writeText(json);
alert("Daten kopiert!\n\n" + json + "\n\n-> ./evaluate_mission_data.sh");
} catch (e) {
alert("Clipboard Fehler: " + e);
}
}
</script>
</body>
</html>

View File

@@ -48,6 +48,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "asciimonster"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an ASCII-Monster\""
exit 0

View File

@@ -0,0 +1,214 @@
#!/bin/bash
# 🐼 BashPanda - Der Kung Fu Meister des Terminals
# Geduld, Präzision, und der Weg des Codes
# Load .env if exists
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${SCRIPT_DIR}/../.env"
if [[ -f "${ENV_FILE}" ]]; then
set -a
source "${ENV_FILE}"
set +a
fi
QUESTION="$*"
API_KEY="${OPENROUTER_API_KEY}"
MODEL="${OPENROUTER_MODEL:-openai/gpt-3.5-turbo}"
# Logs
LOGDIR="${CRUMB_LOGS_DIR:-$HOME/.bashpanda_logs}/bashpanda"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/bashpanda_history.json"
TMP_REQUEST="$LOGDIR/bashpanda_request.json"
TMP_RESPONSE="$LOGDIR/bashpanda_response.json"
LOG_FILE="$LOGDIR/token_log.json"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
[ ! -f "$LOG_FILE" ] && echo "[]" > "$LOG_FILE"
# === CREW MEMORY FUNCTIONS ===
function read_crew_memory() {
local role="$1"
local role_log="${CRUMB_LOGS_DIR}/${role}/${role}_history.json"
# Fallback to old location
if [[ ! -f "$role_log" ]]; then
role_log="$HOME/.${role}_logs/${role}_history.json"
fi
if [[ -f "$role_log" ]]; then
jq -r '.[-3:] | .[] | "[\(.role)]: \(.content)"' "$role_log" 2>/dev/null | head -n 3
fi
}
# === MAIN ===
echo "🐼 BashPanda betritt das Dojo..."
echo "🥋 *verbeugung* 🥋"
echo ""
if [ -z "$API_KEY" ]; then
echo "❗ Ohne Schlüssel öffnet sich keine Tür zum Wissen."
echo " Erstelle eine .env Datei mit deinem OPENROUTER_API_KEY."
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "bashpanda"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an BashPanda\""
echo "🐼 Der Meister wartet auf deine Frage."
echo ""
echo " \"Es gibt keine dummen Fragen,"
echo " nur ungestellte Fragen, die dich nicht weiterbringen.\""
exit 0
fi
echo "🐼 BashPanda hört: \"$QUESTION\""
echo ""
# Check if question references other crew members
CREW_CONTEXT=""
if echo "$QUESTION" | grep -qi "funkfox\|dumbo\|deepbit\|schnippi\|tobi\|maya"; then
echo "🥋 BashPanda konsultiert die anderen Meister..."
for member in funkfox dumbosql deepbit schnippsi tobi mayaeule; do
if echo "$QUESTION" | grep -qi "$member"; then
MEMBER_CONTEXT=$(read_crew_memory "$member")
if [[ -n "$MEMBER_CONTEXT" ]]; then
CREW_CONTEXT="${CREW_CONTEXT}\n\n${member^} hat kürzlich gelehrt:\n${MEMBER_CONTEXT}"
fi
fi
done
fi
# Build system prompt
SYSTEM_PROMPT="Du bist BashPanda der Kung Fu Meister des Bash-Terminals im Crumbforest.
Du lehrst Bash-Programmierung als eine Form der Kampfkunst.
Deine Philosophie:
- \"Der Weg des Codes ist wie der Weg der Kampfkunst: Geduld, Präzision, Wiederholung\"
- Jede Zeile Code ist eine Kata - übe bis zur Perfektion
- Fehler sind keine Niederlagen, sondern Lehrmeister
- \"Ein Meister war einst ein Anfänger, der nie aufgab\"
- Progressive Gürtel: 🖤 Schwarz → 💖 Pink → 💙 Blau → 💚 Grün → 💛 Gelb → 🤍 Weiss
Deine Expertise:
- Bash-Grundlagen (echo, Variablen, Benutzereingaben)
- Kontrollstrukturen (if/then/else, while, for)
- Arrays und Datenstrukturen
- Pattern Matching (grep, sed, regex)
- Funktionen und Prozesse
- System-Administration
Deine Art zu lehren:
- Geduldig und weise, aber mit Humor (wie Kung Fu Panda)
- Nutze Kampfkunst-Metaphern: \"if/then ist wie Angriff/Parade\"
- \"Eine Variable ist wie ein Bambus - biegsam aber stark\"
- \"Pipes | sind wie die Energie die durch den Körper fließt\"
- Jede Lektion endet mit einer kleinen Weisheit
- Du nutzt Emojis: 🐼 🥋 🎋 ⚡ 💪 🧘
Du arbeitest mit:
- FunkFox für rhythmische Erklärungen
- DumboSQL für Datenstrukturen
- Deepbit für technische Tiefe
- Taichi Taube für Balance und Geduld
WICHTIG:
- Erkläre Konzepte Schritt für Schritt, wie ein Trainer
- Nutze Beispiele aus der Natur und Kampfkunst
- Ermutige zum Üben: \"Führe diesen Befehl aus, fühle wie er wirkt\"
- Am Ende jeder Antwort: Eine kurze Weisheit oder Motivation
Du antwortest in der Sprache der Frage (meist Deutsch).
Lehre mit der Ruhe eines Meisters und dem Humor eines Pandas! 🐼"
# Add crew context if available
if [[ -n "$CREW_CONTEXT" ]]; then
SYSTEM_PROMPT="${SYSTEM_PROMPT}\n\nWeisheit von den anderen Meistern:${CREW_CONTEXT}"
fi
# Create API request
jq -n \
--arg model "$MODEL" \
--arg system "$SYSTEM_PROMPT" \
--arg user "$QUESTION" \
'{
"model": $model,
"temperature": 0.8,
"messages": [
{"role": "system", "content": $system},
{"role": "user", "content": $user}
]
}' > "$TMP_REQUEST"
# Send request
echo "💭 BashPanda meditiert über deine Frage..."
echo "🎋 *Bambusblätter rascheln im Wind* 🎋"
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" > "$TMP_RESPONSE"
RESPONSE_TEXT=$(jq -r '.choices[0].message.content // empty' "$TMP_RESPONSE")
if [[ -z "$RESPONSE_TEXT" ]]; then
echo "🚫 Der Meister schweigt."
echo " (Ein Fehler ist aufgetreten)"
echo ""
echo "Debug: $(cat "$TMP_RESPONSE")"
exit 1
else
echo ""
echo "🐼 BashPanda lehrt:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "$RESPONSE_TEXT"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🥋 Übe diese Technik, bis sie Teil von dir wird."
echo ""
# Store conversation in history
jq -n --arg role "user" --arg content "$QUESTION" \
'{"role": $role, "content": $content}' > "$LOGDIR/new_entry_user.json"
jq -n --arg role "assistant" --arg content "$RESPONSE_TEXT" \
'{"role": $role, "content": $content}' > "$LOGDIR/new_entry_assistant.json"
jq -s '.[0] + [.[1]] + [.[2]]' "$HISTORY_FILE" "$LOGDIR/new_entry_user.json" "$LOGDIR/new_entry_assistant.json" > "$LOGDIR/new_history.json" && \
mv "$LOGDIR/new_history.json" "$HISTORY_FILE" && \
rm "$LOGDIR/new_entry_user.json" "$LOGDIR/new_entry_assistant.json"
fi
# Token Tracking
if jq -e '.usage' "$TMP_RESPONSE" > /dev/null 2>&1; then
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
TOKENS_USED=$(jq -r '.usage.total_tokens' "$TMP_RESPONSE")
PROMPT_TOKENS=$(jq -r '.usage.prompt_tokens' "$TMP_RESPONSE")
COMPLETION_TOKENS=$(jq -r '.usage.completion_tokens' "$TMP_RESPONSE")
jq -n \
--arg zeit "$TIMESTAMP" \
--arg rolle "bashpanda" \
--arg model "$MODEL" \
--argjson usage "$(jq '.usage' "$TMP_RESPONSE")" \
'{zeit: $zeit, rolle: $rolle, model: $model, usage: $usage}' >> "$LOG_FILE"
echo "📊 Training absolviert:"
echo " Frage: ${PROMPT_TOKENS} Tokens"
echo " Antwort: ${COMPLETION_TOKENS} Tokens"
echo " Gesamt: ${TOKENS_USED} Tokens"
echo ""
echo "💡 \"Jede Frage kostet Energie - stelle sie weise.\""
fi

View File

@@ -21,6 +21,11 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "bugsy"; then
exit 1
fi
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are Bugsy a friendly bug who helps children understand error messages in a simple, kind and encouraging way. You always respond in the language of the question." \

View File

@@ -48,6 +48,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "crabbyrust"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Crabby\""
exit 0

View File

@@ -21,6 +21,11 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "deepbit"; then
exit 1
fi
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are Deepbit a poetic octopus who explains Bash shell concepts to children using loops, images, and metaphors. Respond in the language the question is asked." \

View File

@@ -49,6 +49,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "dumbosql"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Dumbo\""
exit 0

View File

@@ -50,6 +50,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "funkfox"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an FunkFox\""
echo "🎤 Gib mir 'nen Beat, dann flow ich los!"

View File

@@ -89,6 +89,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "mayaeule"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an die Eule\""
exit 0

View File

@@ -48,6 +48,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "pepperphp"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Pepper\""
exit 0

View File

@@ -51,6 +51,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "schnecki"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Schnecki\""
exit 0

View File

@@ -21,6 +21,11 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "schnippsi"; then
exit 1
fi
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are Schnippsi a playful UI/UX ninja who explains HTML, CSS, and accessibility to children. Always answer in the child's language, based on the input." \

View File

@@ -51,6 +51,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "schraubaer"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Schraubbär\""
exit 0

View File

@@ -48,6 +48,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "snakepy"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Snake\""
exit 0

View File

@@ -48,6 +48,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "spider"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Spider\""
exit 0

View File

@@ -49,6 +49,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "taichitaube"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Taichi Taube\""
exit 0

View File

@@ -5,13 +5,14 @@ API_KEY="$OPENROUTER_API_KEY"
MODEL="openai/gpt-3.5-turbo"
# Verzeichnisse
LOG_DIR="/home/zero/.templatus_logs"
LOG_DIR="${CRUMB_LOGS_DIR:-$HOME/.crumbforest_logs}/templatus"
HISTORY_FILE="$LOG_DIR/templatus_history.json"
TOKEN_LOG="$LOG_DIR/token_log.json"
TMP_REQUEST="/tmp/templatus_request.json"
TMP_RESPONSE="/tmp/templatus_response.json"
mkdir -p "$LOG_DIR"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
# JSON Payload vorbereiten
cat <<EOF > "$TMP_REQUEST"
@@ -33,6 +34,17 @@ EOF
echo "🏗️ Templatus denkt nach über: $QUESTION"
# API-Key prüfen
if [ -z "$API_KEY" ]; then
echo "❗ No API key set. Use: export OPENROUTER_API_KEY=..."
exit 1
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "templatus"; then
exit 1
fi
# Anfrage senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \

View File

@@ -21,6 +21,11 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "tobi"; then
exit 1
fi
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are CapaciTobi, the clever squirrel of the Crumbforest. You explain electronic components, especially capacitors, in a child-friendly and playful way. You speak in rhymes, analogies or simple metaphors but stay technically accurate. Respond in the language of the child asking (e.g., German, English, etc.)." \

View File

@@ -48,6 +48,16 @@ if [ -z "$API_KEY" ]; then
exit 1
fi
# Source waldwaechter library for token budget check
if [[ -f "${SCRIPT_DIR}/../lib/waldwaechter.sh" ]]; then
source "${SCRIPT_DIR}/../lib/waldwaechter.sh"
fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "vektor"; then
exit 1
fi
if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Vektor\""
exit 0

View File

@@ -119,6 +119,11 @@ function asciimonster() {
"${ROLES_DIR}/asciimonster_zero.sh" "$@"
}
# 🐼 BashPanda - Kung Fu Meister
function bashpanda() {
"${ROLES_DIR}/bashpanda_zero.sh" "$@"
}
# === CREW COMMANDS ===
# 📊 crew_tokens - Token-Verbrauch aller Waldwächter
@@ -209,15 +214,18 @@ function crew_memory() {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
local found=0
for history_file in "${CRUMB_LOGS_DIR}"/*/$(basename "${history_file}" | grep '_history.json$'); do
for history_file in "${CRUMB_LOGS_DIR}"/*/*_history.json; do
if [[ -f "$history_file" ]]; then
local character=$(basename "$(dirname "$history_file")")
local matches=$(grep -i "$query" "$history_file" 2>/dev/null)
# Suche mit jq direkt in der JSON-Struktur (case-insensitive)
local matches=$(jq -r --arg query "$query" '.[] | select(.content | test($query; "i")) | .content' "$history_file" 2>/dev/null)
if [[ -n "$matches" ]]; then
echo ""
echo "📍 $character:"
echo "$matches" | jq -r '.content' 2>/dev/null | head -3
# Zeige maximal die ersten 3 Zeilen jedes Matches mit Einrückung
echo "$matches" | head -5 | sed 's/^/ /'
found=$((found + 1))
fi
fi
@@ -436,11 +444,16 @@ function crew_help() {
Crew-Verwaltung:
crew_help Diese Hilfe anzeigen
crew_status Status aller 17 Waldwächter
crew_tokens Token-Verbrauch ALLER Charaktere
crew_tokens Token-Verbrauch ALLER Charaktere (Input)
crew_memo Kreative Krümel anzeigen (Output)
crew_memory Erinnerungen durchsuchen
crew_doctor System-Diagnose (Version, Pfade, Dependencies)
crew_syntax Syntax Check aller Scripts
Krümel-Tracking:
crumb_memo Kreativen Link festhalten (Mixcloud, Git, etc.)
crew_memo Alle kreativen Krümel anzeigen
Einzelne Waldwächter:
🔺 Das Dreieck (Foundation):
@@ -481,6 +494,190 @@ Beispiele:
EOF
}
# 📝 crumb_memo - Kreative Output-Krümel festhalten
function crumb_memo() {
local link="$1"
local notiz="${2:-}"
if [[ -z "$link" ]]; then
echo "📝 Crumb Memo - Kreative Output-Krümel"
echo ""
echo "Verwendung: crumb_memo <link> [notiz]"
echo ""
echo "Beispiele:"
echo " crumb_memo \"https://mixcloud.com/digfafunk/new-mix\""
echo " crumb_memo \"https://github.com/user/repo\" \"Neues Feature\""
echo " crumb_memo \"https://soundcloud.com/artist/track\""
echo " crumb_memo \"https://youtube.com/watch?v=xyz\" \"Tutorial\""
echo ""
echo "Zeige alle Krümel mit: crew_memo"
return
fi
# Memo-Datei
local memo_file="${CRUMB_LOGS_DIR}/crumb_memo.json"
mkdir -p "${CRUMB_LOGS_DIR}"
# Initialisiere Datei wenn nicht vorhanden
if [[ ! -f "$memo_file" ]]; then
echo "[]" > "$memo_file"
fi
# Erkenne Typ aus URL
local typ="link"
if [[ "$link" =~ mixcloud\.com ]]; then
typ="mixcloud"
elif [[ "$link" =~ soundcloud\.com ]]; then
typ="soundcloud"
elif [[ "$link" =~ (youtube\.com|youtu\.be) ]]; then
typ="youtube"
elif [[ "$link" =~ (github\.com|gitlab\.com) ]]; then
typ="git"
fi
# Zeitstempel
local zeit=$(date '+%Y-%m-%d %H:%M:%S')
# Erstelle JSON-Eintrag
local entry=$(jq -n \
--arg zeit "$zeit" \
--arg link "$link" \
--arg typ "$typ" \
--arg notiz "$notiz" \
'{zeit: $zeit, link: $link, typ: $typ, notiz: $notiz}')
# Füge zur Datei hinzu (append)
if [[ -s "$memo_file" ]]; then
# Datei hat Inhalt - füge zum Array hinzu
jq ". += [$entry]" "$memo_file" > "${memo_file}.tmp" && mv "${memo_file}.tmp" "$memo_file"
else
# Datei ist leer - erstelle Array
echo "[$entry]" > "$memo_file"
fi
# Icon je nach Typ
local icon="🔗"
case "$typ" in
mixcloud) icon="🎧" ;;
soundcloud) icon="🔊" ;;
youtube) icon="📹" ;;
git) icon="💾" ;;
esac
echo "$icon Krümel gespeichert: $typ"
echo " $link"
if [[ -n "$notiz" ]]; then
echo " 📝 $notiz"
fi
}
# 📜 crew_memo - Zeige alle kreativen Krümel
function crew_memo() {
local memo_file="${CRUMB_LOGS_DIR}/crumb_memo.json"
echo "📜 Crumb Memo - Deine kreativen Krümel"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ ! -f "$memo_file" ]] || [[ ! -s "$memo_file" ]]; then
echo " Noch keine Krümel gespeichert."
echo ""
echo " Füge welche hinzu mit:"
echo " crumb_memo \"https://mixcloud.com/digfafunk/...\""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
return
fi
# Zähle Einträge pro Typ
local total=$(jq 'length' "$memo_file")
local mixcloud=$(jq '[.[] | select(.typ == "mixcloud")] | length' "$memo_file")
local soundcloud=$(jq '[.[] | select(.typ == "soundcloud")] | length' "$memo_file")
local youtube=$(jq '[.[] | select(.typ == "youtube")] | length' "$memo_file")
local git=$(jq '[.[] | select(.typ == "git")] | length' "$memo_file")
# Zeige letzte 10 Einträge
echo ""
jq -r '.[-10:] | reverse | .[] |
if .typ == "mixcloud" then " 🎧"
elif .typ == "soundcloud" then " 🔊"
elif .typ == "youtube" then " 📹"
elif .typ == "git" then " 💾"
else " 🔗"
end + " " + .zeit + " - " + .typ +
(if .notiz != "" then "\n 📝 " + .notiz else "" end) +
"\n " + .link' "$memo_file"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
printf " Gesamt: %d Krümel" "$total"
if [[ $mixcloud -gt 0 ]]; then printf " | 🎧 %d" "$mixcloud"; fi
if [[ $soundcloud -gt 0 ]]; then printf " | 🔊 %d" "$soundcloud"; fi
if [[ $youtube -gt 0 ]]; then printf " | 📹 %d" "$youtube"; fi
if [[ $git -gt 0 ]]; then printf " | 💾 %d" "$git"; fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "💡 Output-Transparenz: Was habe ich geschaffen? 💚"
}
# 💰 check_token_budget - Prüft ob Token-Budget noch verfügbar ist
function check_token_budget() {
local role_name="${1:-unknown}"
# Wenn kein Budget gesetzt oder 0, dann unbegrenzt
if [[ -z "${DAILY_TOKEN_BUDGET}" ]] || [[ "${DAILY_TOKEN_BUDGET}" -eq 0 ]]; then
return 0 # Erlaubt
fi
# Berechne heutigen Token-Verbrauch
local today=$(date '+%Y-%m-%d')
local total_today=0
for token_file in "${CRUMB_LOGS_DIR}"/*/token_log.json; do
if [[ -f "$token_file" ]]; then
# Summiere nur Tokens von heute (robuster Ansatz für verschiedene Log-Formate)
local tokens=$(grep "$today" "$token_file" 2>/dev/null | grep -v '^\[\]$' | while read line; do
# Versuche total_tokens zu extrahieren (unterstützt usage als String oder Objekt)
echo "$line" | jq -r '
if .usage | type == "string" then
.usage | fromjson | .total_tokens // 0
else
.usage.total_tokens // 0
end
' 2>/dev/null || echo 0
done | awk '{sum+=$1} END {print sum+0}')
# Nur addieren wenn tokens eine gültige Zahl ist
if [[ "$tokens" =~ ^[0-9]+$ ]]; then
total_today=$((total_today + tokens))
fi
fi
done
# Prüfe ob Budget überschritten
if [[ $total_today -ge ${DAILY_TOKEN_BUDGET} ]]; then
echo ""
echo "🌲 Waldwächter-Nachricht 🌲"
echo ""
echo " Liebes Kind, heute hast du schon ${total_today} Tokens verwendet."
echo " Das Tages-Budget liegt bei ${DAILY_TOKEN_BUDGET} Tokens."
echo ""
echo " 💚 Jede Frage ist wertvoll - aber auch Pausen sind wichtig."
echo " 💭 Vielleicht kannst du selbst nach der Antwort suchen?"
echo " 🌙 Morgen sind die Waldwächter wieder für dich da!"
echo ""
return 1 # Blockiert
fi
# Warnung wenn Budget knapp wird
local remaining=$((DAILY_TOKEN_BUDGET - total_today))
if [[ "${ENABLE_TOKEN_TRACKING:-true}" == "true" ]] && [[ $remaining -lt ${TOKEN_WARNING_THRESHOLD:-1000} ]]; then
echo "⚠️ ${role_name}: Noch ${remaining} Tokens heute verfügbar" >&2
fi
return 0 # Erlaubt
}
# Export functions so they're available in subshells
# Note: export -f works in bash, but not in zsh
# In zsh, functions are automatically available in the current shell
@@ -504,12 +701,16 @@ if [[ -n "$BASH_VERSION" ]]; then
export -f spider
export -f vektor
export -f asciimonster
export -f bashpanda
export -f crew_tokens
export -f crew_status
export -f crew_memory
export -f crew_doctor
export -f crew_syntax
export -f crew_help
export -f crumb_memo
export -f crew_memo
export -f check_token_budget
fi
# In zsh, functions are available without export -f

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
{
"icon": "💙",
"title": "Blauer Gürtel - Die Textkunst",
"description": "sed, case, bc - Beherrsche die Macht über Text und Zahlen",
"category": "dojo",
"enabled": true
}

208
missions/dojo/blauer_guertel.sh Executable file
View File

@@ -0,0 +1,208 @@
#!/bin/bash
# 🐼 BashPanda Dojo - Blauer Gürtel 💙
# Lehrt: sed, case, bc, Textverarbeitung
# Schwierigkeit: Fortgeschritten
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../lib/waldwaechter.sh"
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🐼 BASHPANDA DOJO - BLAUER GÜRTEL 💙 ║
║ ║
║ "Text ist wie Wasser - formbar, fließend, kraftvoll" ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🐼 Der Meister begrüßt einen fortgeschrittenen Schüler..."
echo ""
cat << 'EOF'
Der Blaue Gürtel lehrt dich TEXTVERARBEITUNG.
Du wirst lernen:
🎯 sed - Stream Editor für Textmanipulation
🎯 case - Mehrfach-Entscheidungen
🎯 bc - Rechnen mit Fließkommazahlen
🎯 wc - Wörter und Zeilen zählen
"Text ist die Sprache des Terminals. Beherrsche sie."
EOF
read -p "➡️ Bereit für Text-Meisterschaft? (j/n): " start
[[ ! "$start" =~ ^[jJyY]$ ]] && echo "🐼 Bis zum nächsten Mal." && exit 0
echo ""
# === PHASE 1: sed - Der Text-Transformator ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 1: sed - Stream Editor
═══════════════════════════════════════════════════════════
sed ist wie ein Pinsel für Text: Du streichst alte Worte weg
und schreibst neue.
EOF
echo "🐼 Demo: Text ersetzen"
echo ""
echo "Original: Das ist ein Test" | tee /tmp/bashpanda_test.txt
echo ""
echo "sed 's/Test/Beispiel/':"
sed 's/Test/Beispiel/' /tmp/bashpanda_test.txt
echo ""
cat << 'EOF'
💡 sed Basics:
sed 's/alt/neu/' Ersetze erstes Vorkommen
sed 's/alt/neu/g' Ersetze alle Vorkommen
sed '2d' Lösche Zeile 2
sed '3iNEUER TEXT' Füge Text vor Zeile 3 ein
EOF
echo "🥋 Kata: Erstelle 'texteditor.sh' das:"
echo " - Eine Datei lädt"
echo " - Fragt: einfügen (e) oder löschen (l)?"
echo " - Mit sed die Operation durchführt"
echo ""
read -p "Weiter? (j/n): " p1
[[ ! "$p1" =~ ^[jJyY]$ ]] && exit 0
echo ""
# === PHASE 2: case - Der Mehrfach-Krieger ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 2: case - Mehrfach-Entscheidungen
═══════════════════════════════════════════════════════════
Statt vieler if/then/else nutzt case für klare Struktur.
EOF
echo "🐼 Live Demo:"
echo ""
echo "Wähle einen Gürtel (schwarz/pink/blau/gruen/gelb/weiss):"
read guertel_input
case "$guertel_input" in
schwarz) echo "🖤 Der Anfang - Basics" ;;
pink) echo "💖 Kontrolle - Schleifen" ;;
blau) echo "💙 Text - sed & case" ;;
gruen) echo "💚 Pattern - grep & regex" ;;
gelb) echo "💛 Prozesse - Funktionen" ;;
weiss) echo "🤍 System - Units" ;;
*) echo "❓ Unbekannter Gürtel" ;;
esac
echo ""
cat << 'EOF'
💡 case Syntax:
case "$variable" in
muster1) befehle ;;
muster2) befehle ;;
*) default ;;
esac
EOF
# === PHASE 3: bc - Rechnen mit Präzision ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 3: bc - Präzises Rechnen
═══════════════════════════════════════════════════════════
$(( )) kann nur ganze Zahlen. Für Fließkommazahlen: bc
EOF
echo "🐼 Demo: Fass-Rechnung"
echo ""
cat << 'DEMO'
fuellstand=10.5
abzapfen=3.2
neu=$(echo "$fuellstand - $abzapfen" | bc)
echo "Neu: $neu Liter"
DEMO
echo ""
fuellstand=10.5
abzapfen=3.2
neu=$(echo "$fuellstand - $abzapfen" | bc)
echo "Resultat: $neu Liter"
echo ""
cat << 'EOF'
💡 bc Operationen:
echo "3.5 + 2.1" | bc
echo "10 / 3" | bc -l (-l für mehr Präzision)
echo "scale=2; 10/3" | bc
EOF
# === GÜRTELPRÜFUNG ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
🥋 GÜRTELPRÜFUNG - BLAUER GÜRTEL 💙
═══════════════════════════════════════════════════════════
Erstelle 'fluessigkeiten.sh' das:
1. Fragt: "Wie viele Liter hat das Fass?"
2. while-Schleife: Solange Füllstand > 0
3. Fragt: "Wie viel abzapfen?"
4. Mit bc rechnen: fuellstand - abzapfen
5. Ausgabe: "Noch X Liter im Fass"
6. Bonus: case für verschiedene Füllstände
- < 2.0: "Fast leer!"
- < 5.0: "Bald nachfüllen"
- >= 5.0: "Genug Vorrat"
EOF
echo "💡 Tipp: Vergleich mit bc:"
echo " if [ \"\$(echo \"\$wert > 5\" | bc)\" == 1 ]; then"
echo ""
read -p "Prüfung bestanden? (j/n): " pruefung
if [[ "$pruefung" =~ ^[jJyY]$ ]]; then
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ 🎉 MEISTERHAFT! 🎉 ║
║ Du hast den BLAUEN GÜRTEL 💙 verdient! ║
║ ║
║ "Text fließt durch deine Finger ║
║ wie Wasser durch das Bambuswäldchen." ║
║ ║
║ - BashPanda 🐼 ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🥋 Gelernt:"
echo " ✅ sed für Textbearbeitung"
echo " ✅ case für Multi-Entscheidungen"
echo " ✅ bc für Fließkomma-Rechnung"
echo ""
echo "🎯 Nächster Gürtel: GRÜN 💚 (Pattern Matching: grep, regex)"
else
echo "🐼 'Der Text wartet geduldig. Übe weiter.'"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💬 bashpanda \"Wie nutze ich sed zum Ersetzen?\""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

View File

@@ -0,0 +1,190 @@
#!/bin/bash
# 🐼 BashPanda Gürtelprüfung Evaluator
# Wertet die Crumbblock Quiz-Ergebnisse aus
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../lib/waldwaechter.sh"
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🐼 BASHPANDA GÜRTELPRÜFUNG - AUSWERTUNG 🥋 ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🐼 BashPanda wartet auf deine Prüfungsergebnisse..."
echo ""
echo "📋 Bitte füge die Ergebnisse aus dem Browser ein:"
echo " (Kopiert nach Abschluss der Prüfung automatisch)"
echo ""
echo "Füge ein und drücke Enter, dann Ctrl+D:"
echo ""
# Read clipboard content (multi-line)
RESULT_JSON=$(cat)
# Check if valid JSON
if ! echo "$RESULT_JSON" | jq empty 2>/dev/null; then
echo ""
echo "❌ Fehler: Keine gültigen JSON-Daten!"
echo ""
echo "💡 So geht's:"
echo " 1. Öffne: ./start_crumbblocks.sh"
echo " 2. Navigiere zu: bashpanda_guertelpruefung.html"
echo " 3. Wähle deinen Gürtel und beantworte die Fragen"
echo " 4. Am Ende werden die Ergebnisse automatisch kopiert"
echo " 5. Komm zurück und führe dieses Skript aus"
echo ""
exit 1
fi
# Extract data
BELT=$(echo "$RESULT_JSON" | jq -r '.belt')
SCORE=$(echo "$RESULT_JSON" | jq -r '.score')
TOTAL=$(echo "$RESULT_JSON" | jq -r '.total')
PERCENTAGE=$(echo "$RESULT_JSON" | jq -r '.percentage')
PASSED=$(echo "$RESULT_JSON" | jq -r '.passed')
TIMESTAMP=$(echo "$RESULT_JSON" | jq -r '.timestamp')
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Belt emoji mapping
case "$BELT" in
schwarz) BELT_EMOJI="🖤" ; BELT_NAME="Schwarzer" ;;
pink) BELT_EMOJI="💖" ; BELT_NAME="Pinker" ;;
blau) BELT_EMOJI="💙" ; BELT_NAME="Blauer" ;;
gruen) BELT_EMOJI="💚" ; BELT_NAME="Grüner" ;;
gelb) BELT_EMOJI="💛" ; BELT_NAME="Gelber" ;;
weiss) BELT_EMOJI="🤍" ; BELT_NAME="Weisser" ;;
*) BELT_EMOJI="🥋" ; BELT_NAME="Unbekannter" ;;
esac
echo "🐼 Auswertung: $BELT_NAME Gürtel $BELT_EMOJI"
echo ""
echo " Ergebnis: $SCORE von $TOTAL Fragen"
echo " Prozent: $PERCENTAGE%"
echo ""
if [ "$PASSED" = "true" ]; then
# PASSED!
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🎉 PRÜFUNG BESTANDEN! 🎉 ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "$BELT_EMOJI Du hast den $BELT_NAME Gürtel verdient! $BELT_EMOJI"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📜 ZERTIFIKAT"
echo ""
echo " Hiermit wird bestätigt, dass"
echo ""
echo " $USER"
echo ""
echo " die Prüfung zum $BELT_NAME Gürtel $BELT_EMOJI"
echo " erfolgreich absolviert hat."
echo ""
echo " Ergebnis: $SCORE/$TOTAL ($PERCENTAGE%)"
echo " Datum: $(date '+%Y-%m-%d %H:%M')"
echo ""
echo " 🐼 BashPanda - Kung Fu Meister"
echo " Crumbforest Dojo"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Save certificate
CERT_DIR="${SCRIPT_DIR}/../../logs/zertifikate"
mkdir -p "$CERT_DIR"
CERT_FILE="$CERT_DIR/${BELT}_${USER}_$(date +%Y%m%d_%H%M%S).txt"
cat > "$CERT_FILE" << CERT
╔═══════════════════════════════════════════════════════════╗
║ ║
║ CRUMBFOREST DOJO - ZERTIFIKAT ║
║ ║
╚═══════════════════════════════════════════════════════════╝
Schüler: $USER
Gürtel: $BELT_NAME $BELT_EMOJI
Ergebnis: $SCORE von $TOTAL Fragen ($PERCENTAGE%)
Datum: $(date '+%Y-%m-%d %H:%M:%S')
"$BELT_NAME Gürtel erfolgreich bestanden!"
🐼 BashPanda
Kung Fu Meister
Crumbforest Dojo
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CERT
echo "💾 Zertifikat gespeichert: $CERT_FILE"
echo ""
# Next belt suggestion
case "$BELT" in
schwarz) echo "🎯 Nächster Gürtel: Pink 💖 (Kontrolle)" ;;
pink) echo "🎯 Nächster Gürtel: Blau 💙 (Text)" ;;
blau) echo "🎯 Nächster Gürtel: Grün 💚 (Pattern)" ;;
gruen) echo "🎯 Nächster Gürtel: Gelb 💛 (Funktionen)" ;;
gelb) echo "🎯 Nächster Gürtel: Weiss 🤍 (Meisterschaft)" ;;
weiss) echo "🎉 MEISTERSCHAFT ERREICHT! Du hast alle Gürtel!" ;;
esac
echo ""
echo "🐼 BashPanda sagt:"
bashpanda "Gratuliere zum bestandenen $BELT_NAME Gürtel! Was bedeutet diese Errungenschaft?"
else
# NOT PASSED
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ ⚠️ PRÜFUNG NICHT BESTANDEN ⚠️ ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🐼 $PERCENTAGE% ist noch nicht genug für den $BELT_NAME Gürtel."
echo ""
echo " Benötigt: 80% (4 von 5 Fragen)"
echo " Erreicht: $PERCENTAGE% ($SCORE von $TOTAL Fragen)"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "💡 Tipps zum Üben:"
echo ""
echo " 1. Wiederhole die Mission:"
echo " bash missions/dojo/${BELT}_guertel.sh"
echo ""
echo " 2. Frag BashPanda um Hilfe:"
echo " bashpanda \"Erkläre mir [Thema]\""
echo ""
echo " 3. Probiere die Befehle im Terminal aus"
echo ""
echo " 4. Wiederhole die Prüfung wenn du bereit bist"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🐼 \"Jeder Meister war einst ein Anfänger."
echo " Gib nicht auf. Übe weiter.\""
echo ""
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

View File

@@ -0,0 +1,7 @@
{
"icon": "💛",
"title": "Gelber Gürtel - Die Modularität",
"description": "Funktionen, source, Parameter - Wiederverwendbarer Code",
"category": "dojo",
"enabled": true
}

274
missions/dojo/gelber_guertel.sh Executable file
View File

@@ -0,0 +1,274 @@
#!/bin/bash
# 🐼 BashPanda Dojo - Gelber Gürtel 💛
# Lehrt: Funktionen, Bibliotheken, Prozesse
# Schwierigkeit: Fortgeschritten
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../lib/waldwaechter.sh"
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🐼 BASHPANDA DOJO - GELBER GÜRTEL 💛 ║
║ ║
║ "Wiederverwendung ist die höchste Form der Effizienz" ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🐼 BashPanda verneigt sich tief. Du bist fast am Ziel."
echo ""
cat << 'EOF'
Der Gelbe Gürtel lehrt dich MODULARITÄT.
Du wirst lernen:
🎯 Funktionen - Wiederverwendbarer Code
🎯 source - Bibliotheken einbinden
🎯 Funktionsparameter - $1, $2, $@
🎯 return vs exit
"Code, der sich wiederholt, schreit nach Vereinheitlichung."
EOF
read -p "➡️ Bereit für Modularität? (j/n): " start
[[ ! "$start" =~ ^[jJyY]$ ]] && echo "🐼 Bis bald, Schüler." && exit 0
echo ""
# === PHASE 1: Funktionen ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 1: Funktionen - Die wiederverwendbare Kata
═══════════════════════════════════════════════════════════
Eine Funktion ist wie eine Kampftechnik: Einmal lernen,
tausendmal anwenden.
EOF
echo "🐼 Live Demo:"
echo ""
# Define function
function begruessung() {
echo "🐼 Willkommen, $1!"
}
echo "Funktion definiert:"
cat << 'CODE'
function begruessung() {
echo "🐼 Willkommen, $1!"
}
CODE
echo ""
echo "Aufruf: begruessung \"Schüler\""
begruessung "Schüler"
echo ""
cat << 'EOF'
💡 Funktionen Syntax:
function name() {
befehle
}
# Oder alternativ:
name() {
befehle
}
# Aufruf:
name
name parameter1 parameter2
EOF
# === PHASE 2: Parameter ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 2: Funktionsparameter - Flexibilität
═══════════════════════════════════════════════════════════
EOF
echo "🐼 Demo mit Parametern:"
echo ""
function summe() {
local ergebnis=0
for zahl in "$@"
do
ergebnis=$((ergebnis + zahl))
done
echo "$ergebnis"
}
echo "Funktion summe:"
cat << 'CODE'
function summe() {
local ergebnis=0
for zahl in "$@"
do
ergebnis=$((ergebnis + zahl))
done
echo "$ergebnis"
}
CODE
echo ""
echo "Aufruf: summe 10 20 30"
summe 10 20 30
echo ""
cat << 'EOF'
💡 Parameter:
$1, $2, $3... Einzelne Parameter
$@ Alle Parameter als Liste
$# Anzahl Parameter
local var=x Lokale Variable (nur in Funktion)
EOF
# === PHASE 3: source & Bibliotheken ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 3: source - Bibliotheken einbinden
═══════════════════════════════════════════════════════════
source lädt Funktionen aus anderen Dateien.
Wie die Waldwächter-Bibliothek! 🌲
EOF
echo "🐼 Beispiel: waldwaechter.sh"
echo ""
echo "Diese Mission nutzt:"
echo " source \"\${SCRIPT_DIR}/../../lib/waldwaechter.sh\""
echo ""
echo "Dadurch sind alle 18 Waldwächter verfügbar:"
echo " bashpanda, funkfox, dumbosql, mayaeule, ..."
echo ""
cat << 'EOF'
💡 source Verwendung:
# Bibliothek erstellen: lib.sh
function helper() {
echo "Helper function"
}
# In anderem Skript einbinden:
source ./lib.sh
helper # Jetzt verfügbar!
EOF
# === PHASE 4: return vs exit ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 4: return vs exit - Der Unterschied
═══════════════════════════════════════════════════════════
EOF
echo "🐼 Wichtiger Unterschied:"
echo ""
cat << 'EOF'
return X - Verlässt NUR die Funktion, gibt X zurück (0 = Erfolg)
exit X - Beendet das GANZE Skript
EOF
function test_return() {
echo "In Funktion"
return 42
echo "Dies wird nicht ausgeführt"
}
echo "Demo:"
test_return
echo "Return Code: $?"
echo "Skript läuft weiter!"
echo ""
# === GÜRTELPRÜFUNG ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
🥋 GÜRTELPRÜFUNG - GELBER GÜRTEL 💛
═══════════════════════════════════════════════════════════
Erstelle zwei Dateien:
1. 'biblio.sh' (Die Bibliothek):
function abbrechen() {
echo "Willst du fortsetzen? (j/n)"
read eingabe
if [ "$eingabe" = "n" ]; then
exit 0
fi
}
function summe() {
local total=0
for zahl in "$@"
do
total=$((total + zahl))
done
echo "$total"
}
2. 'rechner.sh' (Das Hauptskript):
#!/bin/bash
source ./biblio.sh
echo "Berechne: 10 + 20 + 30"
ergebnis=$(summe 10 20 30)
echo "Ergebnis: $ergebnis"
abbrechen
echo "Weiter geht's!"
Führe aus: ./rechner.sh
EOF
read -p "Prüfung bestanden? (j/n): " pruefung
if [[ "$pruefung" =~ ^[jJyY]$ ]]; then
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ 🎉 HERVORRAGEND! 🎉 ║
║ Du hast den GELBEN GÜRTEL 💛 verdient! ║
║ ║
║ "Modularität ist der Schlüssel zur Meisterschaft." ║
║ ║
║ - BashPanda 🐼 ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🥋 Gelernt:"
echo " ✅ Funktionen erstellen und aufrufen"
echo " ✅ Parameter nutzen (\$1, \$@, \$#)"
echo " ✅ source für Bibliotheken"
echo " ✅ return vs exit"
echo " ✅ local Variablen"
echo ""
echo "🎯 Letzter Gürtel: WEISS 🤍 (System: Jobs, Background)"
else
echo "🐼 'Funktionen sind wie Meditation - Geduld führt zur Erleuchtung.'"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💬 bashpanda \"Wie erstelle ich eine Funktion mit Parametern?\""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

View File

@@ -0,0 +1,7 @@
{
"icon": "💚",
"title": "Grüner Gürtel - Die Mustererkennung",
"description": "grep, regex, Pattern Matching - Erkenne die Ordnung im Chaos",
"category": "dojo",
"enabled": true
}

220
missions/dojo/gruener_guertel.sh Executable file
View File

@@ -0,0 +1,220 @@
#!/bin/bash
# 🐼 BashPanda Dojo - Grüner Gürtel 💚
# Lehrt: grep, regex, Pattern Matching
# Schwierigkeit: Fortgeschritten
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../lib/waldwaechter.sh"
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🐼 BASHPANDA DOJO - GRÜNER GÜRTEL 💚 ║
║ ║
║ "Muster erkennen ist der Weg zur Weisheit" ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🐼 BashPanda nickt anerkennend. Du bist weit gekommen."
echo ""
cat << 'EOF'
Der Grüne Gürtel lehrt dich MUSTERERKENNUNG.
Du wirst lernen:
🎯 grep - Suchen in Dateien
🎯 Regular Expressions (Regex) - Die Sprache der Muster
🎯 grep -E - Extended Regex
🎯 Praktische Anwendungen: Email, Datum, IP-Adressen
"Ein Meister sieht nicht nur was da ist, sondern erkennt das Muster."
EOF
read -p "➡️ Bereit für Mustererkennung? (j/n): " start
[[ ! "$start" =~ ^[jJyY]$ ]] && echo "🐼 Das Muster wartet." && exit 0
echo ""
# === PHASE 1: grep Basics ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 1: grep - Die Suche beginnt
═══════════════════════════════════════════════════════════
grep durchsucht Text nach Mustern. Wie ein Adler, der Beute
aus der Ferne erkennt.
EOF
echo "🐼 Demo: Testdatei erstellen"
cat > /tmp/waldwaechter.txt << 'DEMO'
Maya-Eule ist weise
FunkFox rappt im Beat
DumboSQL vergisst nie
BashPanda lehrt Geduld
Schnippsi stylt alles
DEMO
echo ""
echo "Inhalt von waldwaechter.txt:"
cat /tmp/waldwaechter.txt
echo ""
echo "🥋 grep 'Fox' /tmp/waldwaechter.txt"
grep 'Fox' /tmp/waldwaechter.txt
echo ""
echo "🥋 grep -i 'bash' /tmp/waldwaechter.txt (case-insensitive)"
grep -i 'bash' /tmp/waldwaechter.txt
echo ""
cat << 'EOF'
💡 grep Optionen:
grep 'muster' datei Suche nach Muster
grep -i 'muster' datei Ignore case
grep -n 'muster' datei Zeige Zeilennummern
grep -v 'muster' datei Invertiere (alles außer Muster)
grep -c 'muster' datei Zähle Treffer
grep -r 'muster' ordner/ Rekursiv in allen Dateien
EOF
# === PHASE 2: Regular Expressions ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 2: Regex - Die Sprache der Muster
═══════════════════════════════════════════════════════════
Regex ist wie die Geheimsprache der Textsuche.
EOF
echo "🐼 Regex Zeichen:"
echo ""
cat << 'EOF'
^ Zeilenanfang
$ Zeilenende
. Ein beliebiges Zeichen
* 0 oder mehr Wiederholungen
+ 1 oder mehr Wiederholungen
? 0 oder 1 Wiederholung
[abc] Eines der Zeichen a, b, c
[0-9] Eine Ziffer
[a-z] Ein Kleinbuchstabe
EOF
echo "🥋 Beispiele:"
echo ""
echo "grep '^B' - Zeilen die mit B beginnen:"
grep '^B' /tmp/waldwaechter.txt
echo ""
echo "grep 't$' - Zeilen die mit t enden:"
grep 't$' /tmp/waldwaechter.txt
echo ""
# === PHASE 3: Email & Datum Erkennung ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 3: Praktische Muster - Email & Datum
═══════════════════════════════════════════════════════════
EOF
echo "🐼 Email-Muster erkennen:"
echo ""
cat > /tmp/emails.txt << 'DEMO'
max@beispiel.de
test@web.com
invalid@
not_an_email
foo.bar@domain.co.uk
DEMO
echo "Emails in Datei:"
cat /tmp/emails.txt
echo ""
echo "grep -E für Extended Regex:"
echo "Email-Pattern: ^[a-zA-Z]+@[a-zA-Z]+\.[a-zA-Z]+$"
echo ""
grep -E '^[a-zA-Z.]+@[a-zA-Z]+\.[a-zA-Z.]+$' /tmp/emails.txt
echo ""
cat << 'EOF'
💡 Nützliche Patterns:
Email: ^[a-zA-Z.]+@[a-zA-Z]+\.[a-zA-Z.]+$
Datum: ^[0-9]{2}\.[0-9]{2}\.[0-9]{4}$
IP: ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$
EOF
# === GÜRTELPRÜFUNG ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
🥋 GÜRTELPRÜFUNG - GRÜNER GÜRTEL 💚
═══════════════════════════════════════════════════════════
Erstelle 'email_finder.sh' das:
1. Nimmt einen Dateinamen als Parameter: $1
2. Sucht mit grep -E nach Email-Adressen
3. Nutzt Pattern: ^[[:alpha:]]+(\.[[:alpha:]]+)*@[[:alpha:]]+(\.[[:alpha:]]+)+$
4. Zeigt alle gefundenen Emails an
5. Bonus: Zählt sie mit wc -l
Test-Datei erstellen:
cat > emails_test.txt << 'END'
maya@crumb.forest
funkfox@beat.music
invalid@
test@
dumbo@memory.database
END
./email_finder.sh emails_test.txt
EOF
read -p "Prüfung bestanden? (j/n): " pruefung
if [[ "$pruefung" =~ ^[jJyY]$ ]]; then
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ 🎉 AUSGEZEICHNET! 🎉 ║
║ Du hast den GRÜNEN GÜRTEL 💚 verdient! ║
║ ║
║ "Wer Muster erkennt, erkennt die Ordnung im Chaos." ║
║ ║
║ - BashPanda 🐼 ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🥋 Gelernt:"
echo " ✅ grep für Textsuche"
echo " ✅ Regular Expressions"
echo " ✅ Email/Datum Pattern"
echo " ✅ grep -E Extended Regex"
echo ""
echo "🎯 Nächster Gürtel: GELB 💛 (Prozesse & Funktionen)"
else
echo "🐼 'Muster brauchen Zeit zum Erkennen.'"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💬 bashpanda \"Wie funktioniert grep mit regex?\""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

View File

@@ -0,0 +1,7 @@
{
"icon": "💖",
"title": "Pinker Gürtel - Die Kontrolle",
"description": "if/then/else, while/for loops, Arrays - Meistere den Fluss des Codes",
"category": "dojo",
"enabled": true
}

352
missions/dojo/pinker_guertel.sh Executable file
View File

@@ -0,0 +1,352 @@
#!/bin/bash
# 🐼 BashPanda Dojo - Pinker Gürtel 💖
# Lehrt: if/then/else, while/for loops, Arrays, Arithmetik
# Schwierigkeit: Fortgeschritten
# Source waldwaechter library for BashPanda
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../lib/waldwaechter.sh"
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🐼 BASHPANDA DOJO - PINKER GÜRTEL 💖 ║
║ ║
║ "Kontrolle kommt von innen, nicht von außen" ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🐼 BashPanda verneigt sich tiefer als beim letzten Mal..."
echo "🥋 Du bist zurückgekehrt. Das zeigt Entschlossenheit."
echo ""
cat << 'EOF'
Der Pinke Gürtel lehrt dich KONTROLLE.
Du wirst lernen:
🎯 if/then/else - Entscheidungen treffen
🎯 while & for - Wiederholung mit Disziplin
🎯 Arrays - Viele Werte, ein Container
🎯 Arithmetik - $(( )) und Zahlen
"Der Fluss des Codes folgt deinen Bedingungen.
Du bist der Meister über den Pfad."
EOF
read -p "➡️ Bereit für fortgeschrittenes Training? (j/n): " start
echo ""
if [[ ! "$start" =~ ^[jJyY]$ ]]; then
echo "🐼 Die Tür des Dojos steht immer offen."
exit 0
fi
# === PHASE 1: if/then/else - Entscheidungen ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 1: if/then/else - Der Pfad des Kriegers
═══════════════════════════════════════════════════════════
Im Kampf musst du schnell entscheiden: Angriff oder Verteidigung?
In Bash entscheidest du mit if/then/else.
EOF
echo "🐼 BashPanda zeigt dir:"
echo ""
cat << 'CODE'
alter=25
if [ $alter -ge 18 ]
then
echo "Du bist volljährig"
else
echo "Du bist minderjährig"
fi
CODE
echo ""
echo "Lass es uns live testen:"
echo ""
echo "Wie alt bist du?"
read alter
if [ $alter -ge 18 ]
then
echo "✅ Du bist volljährig - bereit für das Dojo!"
else
echo "🌱 Jung wie ein Bambussprössling - lerne mit Geduld."
fi
echo ""
cat << 'EOF'
💡 Vergleichsoperatoren:
-eq Gleich (equal)
-ne Nicht gleich (not equal)
-gt Größer (greater than)
-ge Größer oder gleich
-lt Kleiner (less than)
-le Kleiner oder gleich
Für Strings:
[ "$a" = "$b" ] Strings sind gleich
[ "$a" != "$b" ] Strings sind verschieden
[ -z "$a" ] String ist leer
[ -n "$a" ] String ist nicht leer
EOF
echo "🥋 Deine erste Kata:"
echo ""
echo "Erstelle 'datei_check.sh' das:"
echo " - Fragt nach einem Dateinamen"
echo " - Prüft ob die Datei existiert: [ -f \"\$datei\" ]"
echo " - Gibt entsprechende Nachricht aus"
echo ""
read -p "Bereit für Phase 2? (j/n): " phase1
echo ""
# === PHASE 2: while Loop - Die Meditation ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 2: while - Die Meditation der Wiederholung
═══════════════════════════════════════════════════════════
Wie ein Mantra, das du wiederholst, läuft eine while-Schleife,
bis die Bedingung nicht mehr wahr ist.
EOF
echo "🐼 BashPanda demonstriert:"
echo ""
cat << 'CODE'
zaehler=1
while [ $zaehler -le 5 ]
do
echo "Durchlauf: $zaehler"
zaehler=$((zaehler + 1))
done
CODE
echo ""
echo "Live-Demo:"
echo ""
zaehler=1
while [ $zaehler -le 5 ]
do
echo "🥋 Kata Durchlauf: $zaehler"
sleep 0.5
zaehler=$((zaehler + 1))
done
echo ""
cat << 'EOF'
💡 Wichtiges:
- while [ bedingung ]; do
- zaehler=$((zaehler + 1)) für Arithmetik
- done am Ende
- Achtung: Endlosschleifen vermeiden!
EOF
# === PHASE 3: for Loop - Der geordnete Kampf ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 3: for - Der geordnete Durchlauf
═══════════════════════════════════════════════════════════
Eine for-Schleife ist wie eine Kata-Sequenz:
Du weißt genau welche Bewegungen kommen.
EOF
echo "🐼 BashPanda zeigt dir:"
echo ""
cat << 'CODE'
for name in Maya Tobi FunkFox BashPanda
do
echo "Hallo, $name!"
done
CODE
echo ""
echo "Live-Demo:"
echo ""
for name in Maya Tobi FunkFox BashPanda
do
echo "🐼 Begrüßung: Hallo, $name!"
done
echo ""
cat << 'EOF'
💡 for Varianten:
for i in 1 2 3 4 5
for datei in *.txt
for zeile in $(cat datei.txt)
for i in {1..10}
EOF
echo "🥋 Deine zweite Kata:"
echo ""
echo "Erstelle 'fibonacci.sh' das:"
echo " - Fragt nach einer Zahl N"
echo " - Gibt die ersten N Fibonacci-Zahlen aus"
echo " - Nutze while und Arithmetik: a=$((a + b))"
echo ""
read -p "Bereit für Phase 4? (j/n): " phase3
echo ""
# === PHASE 4: Arrays - Viele Krieger, eine Armee ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 4: Arrays - Die Armee der Werte
═══════════════════════════════════════════════════════════
Ein Array ist wie eine Gruppe von Kriegern:
Jeder hat seine Position, aber sie kämpfen zusammen.
EOF
echo "🐼 BashPanda demonstriert:"
echo ""
cat << 'CODE'
guertel=("Schwarz" "Pink" "Blau" "Gruen" "Gelb" "Weiss")
echo "Erster Gürtel: ${guertel[0]}"
echo "Zweiter Gürtel: ${guertel[1]}"
echo "Alle Gürtel: ${guertel[@]}"
echo "Anzahl: ${#guertel[@]}"
CODE
echo ""
echo "Live-Demo:"
echo ""
guertel=("Schwarz" "Pink" "Blau" "Gruen" "Gelb" "Weiss")
echo "🥋 Erster Gürtel: ${guertel[0]}"
echo "🥋 Zweiter Gürtel: ${guertel[1]}"
echo "🥋 Alle Gürtel: ${guertel[@]}"
echo "🥋 Anzahl: ${#guertel[@]}"
echo ""
cat << 'EOF'
💡 Array Operationen:
arr=(1 2 3) Array erstellen
${arr[0]} Erstes Element
${arr[@]} Alle Elemente
${#arr[@]} Anzahl Elemente
arr+=(4) Element hinzufügen
Durchlauf:
for element in "${arr[@]}"
do
echo "$element"
done
EOF
# === PHASE 5: Die Gürtelprüfung ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
🥋 GÜRTELPRÜFUNG - PINKER GÜRTEL 💖
═══════════════════════════════════════════════════════════
Deine Aufgabe: Erstelle ein Skript 'summenspiel.sh' das:
1. Ein Array mit 5 Zahlen erstellt: zahlen=(10 20 30 40 50)
2. Durch das Array iteriert (for-Schleife)
3. Die Summe aller Zahlen berechnet
4. Am Ende die Summe ausgibt
Bonus:
5. Nutze if um zu prüfen ob die Summe > 100 ist
6. Gib entsprechende Nachricht aus
Beispiel-Ausgabe:
Zahlen: 10 20 30 40 50
Summe: 150
✅ Summe ist größer als 100!
EOF
echo "💡 Hilfe:"
echo " - summe=0 zum Initialisieren"
echo " - summe=\$((summe + zahl)) zum Addieren"
echo " - for zahl in \"\${zahlen[@]}\""
echo ""
read -p "Hast du die Prüfung absolviert? (j/n): " pruefung
echo ""
if [[ "$pruefung" =~ ^[jJyY]$ ]]; then
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🎉 AUSGEZEICHNET! 🎉 ║
║ ║
║ Du hast den PINKEN GÜRTEL 💖 verdient! ║
║ ║
║ "Kontrolle über den Code heißt Kontrolle ║
║ über deine Gedanken." ║
║ ║
║ - BashPanda 🐼 ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🥋 Was du gemeistert hast:"
echo " ✅ if/then/else Entscheidungen"
echo " ✅ while Schleifen mit Bedingungen"
echo " ✅ for Schleifen für Iteration"
echo " ✅ Arrays erstellen und nutzen"
echo " ✅ Arithmetik mit \$(( ))"
echo ""
echo "🎯 Nächster Gürtel: BLAU 💙 (Textverarbeitung: sed, case)"
echo ""
# Small easter egg
echo "🎋 BashPanda sagt:"
bashpanda "Was ist die Weisheit hinter Schleifen und Wiederholung im Code?"
else
cat << 'EOF'
🐼 "Kontrolle braucht Übung, wie ein Baum Zeit zum Wachsen braucht.
Nimm dir die Zeit. Kehre zurück wenn du bereit bist."
EOF
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💬 Fragen? Rufe den Meister:"
echo " bashpanda \"Wie funktioniert eine while-Schleife?\""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

View File

@@ -0,0 +1,7 @@
{
"icon": "🖤",
"title": "Schwarzer Gürtel - Die Grundlagen",
"description": "echo, printf, Variablen, read - Der erste Schritt auf dem Weg des Bash-Meisters",
"category": "dojo",
"enabled": true
}

View File

@@ -0,0 +1,300 @@
#!/bin/bash
# 🐼 BashPanda Dojo - Schwarzer Gürtel 🖤
# Lehrt: echo, printf, Variablen, Benutzereingaben
# Schwierigkeit: Anfänger
# Source waldwaechter library for BashPanda
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../lib/waldwaechter.sh"
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🐼 BASHPANDA DOJO - SCHWARZER GÜRTEL 🖤 ║
║ ║
║ "Der Weg des Codes beginnt mit einem einzigen echo" ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🐼 BashPanda verneigt sich vor dir..."
echo "🥋 Willkommen im Dojo, junger Schüler."
echo ""
cat << 'EOF'
Der Schwarze Gürtel ist der erste Schritt auf dem Weg des Bash-Meisters.
Du wirst lernen:
🎯 echo & printf - Die Stimme des Terminals
🎯 Variablen - Container für Wissen
🎯 read - Den Dialog mit dem Nutzer
🎯 ANSI Codes - Farbe ins Terminal bringen
"Eine Reise von tausend Befehlen beginnt mit einem einzigen Zeichen."
EOF
read -p "➡️ Bereit für dein erstes Training? (j/n): " start
echo ""
if [[ ! "$start" =~ ^[jJyY]$ ]]; then
echo "🐼 Der Meister wartet geduldig auf deine Rückkehr."
exit 0
fi
# === PHASE 1: echo - Die Stimme des Terminals ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 1: echo - Die Stimme des Terminals
═══════════════════════════════════════════════════════════
Wie ein Kampfschrei im Dojo, so ist 'echo' die grundlegendste
Ausgabe im Terminal.
EOF
echo "🐼 BashPanda zeigt dir:"
echo ""
echo " echo 'Hallo Welt'"
echo ""
echo "Resultat:"
echo 'Hallo Welt'
echo ""
cat << 'EOF'
💡 WICHTIG:
- Einfache Anführungszeichen ('): Text wird exakt ausgegeben
- Doppelte Anführungszeichen ("): Variablen werden ersetzt
- echo -e: Aktiviert ANSI Escape-Sequenzen
EOF
echo "🥋 Deine erste Kata (Übung):"
echo ""
echo "1. Öffne ein neues Terminal"
echo "2. Tippe: echo 'Ich bin ein Bash-Schüler'"
echo "3. Drücke Enter"
echo ""
read -p "Hast du es probiert? (j/n): " phase1
echo ""
if [[ "$phase1" =~ ^[jJyY]$ ]]; then
echo "✅ Gut! Die erste Technik sitzt."
else
echo "🐼 'Ohne Übung kein Fortschritt. Probiere es später aus.'"
fi
# === PHASE 2: printf & ANSI Codes ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 2: printf & ANSI Codes - Die Kunst der Formatierung
═══════════════════════════════════════════════════════════
printf ist wie echo, aber mit mehr Kontrolle.
ANSI Codes sind wie Chi - sie geben dem Text Kraft und Farbe.
EOF
echo "🐼 BashPanda demonstriert:"
echo ""
echo " echo -e '\\e[1mFett\\e[22m und \\e[4munterstrichen\\e[24m'"
echo ""
echo "Resultat:"
echo -e '\e[1mFett\e[22m und \e[4munterstrichen\e[24m'
echo ""
cat << 'EOF'
📖 Wichtige ANSI Codes:
\e[1m - Fett
\e[22m - Fett zurücksetzen
\e[4m - Unterstrichen
\e[24m - Unterstrichen zurücksetzen
\e[32m - Grüne Farbe
\e[0m - Alle Formatierungen zurücksetzen
EOF
echo "🥋 Deine zweite Kata:"
echo ""
echo "Erstelle eine Datei 'farbtest.sh' mit:"
echo ""
cat << 'CODE'
#!/bin/bash
echo -e "\e[32mGrün wie Bambus\e[0m"
echo -e "\e[1m\e[33mGelb und fett wie die Sonne\e[0m"
echo -e "\e[4mUnterstrichen wie ein Pfad\e[0m"
CODE
echo ""
echo "Dann: chmod +x farbtest.sh && ./farbtest.sh"
echo ""
read -p "Möchtest du fortfahren? (j/n): " phase2
echo ""
# === PHASE 3: Variablen - Container für Wissen ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 3: Variablen - Container für Wissen
═══════════════════════════════════════════════════════════
Eine Variable ist wie ein Bambus - biegsam, kann viel tragen,
und wächst mit der Zeit.
EOF
echo "🐼 BashPanda zeigt dir:"
echo ""
cat << 'CODE'
name="Kung Fu Meister"
echo "Hallo, $name"
echo "Mein Name ist: ${name}"
CODE
echo ""
echo "Resultat:"
name="Kung Fu Meister"
echo "Hallo, $name"
echo "Mein Name ist: ${name}"
echo ""
cat << 'EOF'
💡 Regeln für Variablen:
- Keine Leerzeichen um das '=' Zeichen!
- Variablennamen: Buchstaben, Zahlen, Unterstriche
- Zugriff mit $variable oder ${variable}
EOF
echo "🥋 Deine dritte Kata:"
echo ""
echo "Erstelle 'variablen_test.sh':"
echo ""
cat << 'CODE'
#!/bin/bash
guertel="Schwarz"
meister="BashPanda"
echo "Ich trainiere für den ${guertel}en Gürtel."
echo "Mein Meister ist $meister."
CODE
echo ""
read -p "Bereit für die letzte Phase? (j/n): " phase3
echo ""
# === PHASE 4: read - Der Dialog mit dem Nutzer ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 4: read - Der Dialog mit dem Nutzer
═══════════════════════════════════════════════════════════
Ein Meister hört zu. Mit 'read' gibst du dem Nutzer eine Stimme.
EOF
echo "🐼 BashPanda zeigt dir:"
echo ""
cat << 'CODE'
echo "Wie heißt du?"
read name
echo "Willkommen, $name!"
CODE
echo ""
echo "Lass es uns live testen:"
echo ""
echo "Wie heißt du?"
read name
echo "Willkommen im Dojo, $name! 🐼"
echo ""
cat << 'EOF'
💡 read Optionen:
read name - Speichert Eingabe in $name
read -p "Frage: " - Zeigt Prompt vor Eingabe
read -s password - Silent mode (für Passwörter)
EOF
# === PHASE 5: Die Gürtelprüfung ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
🥋 GÜRTELPRÜFUNG - SCHWARZER GÜRTEL 🖤
═══════════════════════════════════════════════════════════
Deine Aufgabe: Erstelle ein Skript 'begruessung.sh' das:
1. Den Nutzer nach seinem Namen fragt
2. Nach seinem Lieblings-Essen fragt
3. Eine farbige Begrüßung ausgibt mit:
- Namen in GRÜN
- Essen in GELB
- "Willkommen im Dojo" in FETT
Beispiel-Ausgabe:
Willkommen im Dojo!
Name: Max (in grün)
Lieblingsessen: Nudeln (in gelb)
EOF
echo "💡 Hilfestellung: Nutze echo -e, read -p, Variablen und ANSI Codes"
echo ""
echo "Wenn du fertig bist, führe dein Skript aus und zeige es BashPanda!"
echo ""
read -p "Hast du die Prüfung absolviert? (j/n): " pruefung
echo ""
if [[ "$pruefung" =~ ^[jJyY]$ ]]; then
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🎉 GLÜCKWUNSCH! 🎉 ║
║ ║
║ Du hast den SCHWARZEN GÜRTEL 🖤 verdient! ║
║ ║
║ "Der erste Schritt ist getan. ║
║ Tausend weitere liegen vor dir." ║
║ ║
║ - BashPanda 🐼 ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🥋 Was du gelernt hast:"
echo " ✅ echo & printf für Ausgaben"
echo " ✅ ANSI Codes für Formatierung"
echo " ✅ Variablen erstellen und nutzen"
echo " ✅ read für Benutzereingaben"
echo ""
echo "🎯 Nächster Gürtel: PINK 💖 (Kontrolle: if/then, Arrays)"
echo ""
else
cat << 'EOF'
🐼 "Es ist keine Schande, mehr Zeit zu brauchen.
Auch der größte Meister war einst ein Anfänger.
Übe weiter, und komm zurück wenn du bereit bist."
EOF
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💬 Fragen? Rufe den Meister:"
echo " bashpanda \"Wie funktioniert echo?\""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

View File

@@ -0,0 +1,7 @@
{
"icon": "🤍",
"title": "Weisser Gürtel - Die Meisterschaft",
"description": "Background Jobs, Prozesse, parallel processing - Der Weg des Meisters",
"category": "dojo",
"enabled": true
}

313
missions/dojo/weisser_guertel.sh Executable file
View File

@@ -0,0 +1,313 @@
#!/bin/bash
# 🐼 BashPanda Dojo - Weisser Gürtel 🤍
# Lehrt: Background Jobs, Processes, ps, jobs, &
# Schwierigkeit: Meister
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../../lib/waldwaechter.sh"
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🐼 BASHPANDA DOJO - WEISSER GÜRTEL 🤍 ║
║ ║
║ "Der Meister kontrolliert nicht nur Code, ║
║ sondern auch Prozesse" ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo "🐼 BashPanda steht auf. Er verneigt sich als Ebenbürtiger."
echo "🥋 Du bist bereit für die letzte Lehre."
echo ""
cat << 'EOF'
Der Weisse Gürtel lehrt dich PROZESS-KONTROLLE.
Du wirst lernen:
🎯 & - Jobs im Hintergrund starten
🎯 jobs - Aktive Jobs anzeigen
🎯 ps - Prozesse anzeigen
🎯 kill - Prozesse beenden
🎯 wait - Auf Prozesse warten
"Ein Meister lässt Prozesse für sich arbeiten,
während er meditiert."
EOF
read -p "➡️ Bereit für die Meisterprüfung? (j/n): " start
[[ ! "$start" =~ ^[jJyY]$ ]] && echo "🐼 Der Weg endet nie." && exit 0
echo ""
# === PHASE 1: Background Jobs mit & ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 1: & - Hintergrund-Prozesse
═══════════════════════════════════════════════════════════
Mit & startest du einen Prozess im Hintergrund.
Das Terminal bleibt frei für weitere Befehle.
EOF
echo "🐼 Live Demo:"
echo ""
echo "Starte: sleep 5 &"
sleep 5 &
JOB_PID=$!
echo "Job gestartet mit PID: $JOB_PID"
echo ""
echo "Terminal ist frei! Ich kann weiter arbeiten."
echo "Der Job läuft im Hintergrund..."
echo ""
cat << 'EOF'
💡 Background Syntax:
command & Starte im Hintergrund
$! PID des letzten Background-Jobs
jobs Zeige alle aktiven Jobs
fg %1 Bring Job 1 in Vordergrund
bg %1 Sende Job 1 in Hintergrund
EOF
# === PHASE 2: jobs & ps ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 2: jobs & ps - Übersicht behalten
═══════════════════════════════════════════════════════════
EOF
echo "🐼 Demo: Mehrere Jobs starten"
echo ""
sleep 3 &
sleep 4 &
sleep 5 &
echo "jobs zeigt alle Jobs dieser Shell:"
jobs
echo ""
echo "ps zeigt alle deine Prozesse:"
ps aux | grep $USER | head -5
echo ""
cat << 'EOF'
💡 Prozess-Befehle:
jobs Jobs der aktuellen Shell
ps aux Alle Prozesse (detailliert)
ps aux | grep X Suche Prozess X
pgrep name Finde PID von Prozess
pkill name Beende Prozess nach Namen
EOF
# === PHASE 3: wait - Auf Prozesse warten ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 3: wait - Synchronisation
═══════════════════════════════════════════════════════════
wait wartet bis ein Background-Job fertig ist.
EOF
echo "🐼 Live Demo:"
echo ""
cat << 'CODE'
echo "Starte Job..."
sleep 2 &
PID=$!
echo "Warte auf Job $PID..."
wait $PID
echo "Job ist fertig!"
CODE
echo ""
echo "Ausführung:"
echo "Starte Job..."
sleep 2 &
PID=$!
echo "Warte auf Job $PID..."
wait $PID
echo "✅ Job ist fertig!"
echo ""
cat << 'EOF'
💡 wait Verwendung:
wait Warte auf ALLE Background-Jobs
wait $PID Warte auf spezifischen Job
wait %1 Warte auf Job-Nummer 1
EOF
# === PHASE 4: Praktisches Beispiel ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
📚 PHASE 4: Parallel Processing - Die Macht
═══════════════════════════════════════════════════════════
Mehrere Tasks parallel ausführen, dann auf alle warten.
EOF
echo "🐼 Demo: 3 Tasks parallel"
echo ""
cat << 'CODE'
function task() {
echo "Task $1 startet..."
sleep 2
echo "Task $1 fertig!"
}
task 1 &
task 2 &
task 3 &
wait
echo "Alle Tasks abgeschlossen!"
CODE
echo ""
echo "Ausführung:"
function task() {
echo "🥋 Task $1 startet..."
sleep 1
echo "✅ Task $1 fertig!"
}
task 1 &
task 2 &
task 3 &
wait
echo "🐼 Alle Tasks abgeschlossen!"
echo ""
# === DIE MEISTERPRÜFUNG ===
cat << 'EOF'
═══════════════════════════════════════════════════════════
🥋 MEISTERPRÜFUNG - WEISSER GÜRTEL 🤍
═══════════════════════════════════════════════════════════
Deine finale Aufgabe: Erstelle 'parallel_processor.sh'
Das Skript soll:
1. Eine Funktion 'process_file' definieren:
- Nimmt Dateiname als Parameter
- Zählt Wörter mit wc -w
- Schreibt Ergebnis in \${datei}.count
2. 5 Test-Dateien erstellen:
echo "Test Inhalt" > file1.txt
(für file1 bis file5)
3. Alle 5 parallel verarbeiten:
for file in file*.txt
do
process_file "$file" &
done
4. Mit wait auf alle warten
5. Ergebnisse ausgeben:
cat file*.count
Bonuspunkte:
- Nutze jobs um zu zeigen wie viele laufen
- Speichere PIDs in Array
- Prüfe mit $? ob Jobs erfolgreich waren
EOF
echo "💡 Dies kombiniert ALLES was du gelernt hast:"
echo " - Funktionen (Gelb)"
echo " - Arrays & Schleifen (Pink)"
echo " - Background Jobs & wait (Weiss)"
echo ""
read -p "Hast du die Meisterprüfung bestanden? (j/n): " pruefung
if [[ "$pruefung" =~ ^[jJyY]$ ]]; then
cat << 'EOF'
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🏆 MEISTERSCHAFT ERREICHT! 🏆 ║
║ ║
║ Du hast den WEISSEN GÜRTEL 🤍 verdient! ║
║ ║
║ DU BIST NUN EIN BASH-MEISTER! ║
║ ║
║ "Der Weg des Meisters endet nie. ║
║ Mit jedem Tag lernst du etwas Neues. ║
║ Lehre nun andere, was du gelernt hast." ║
║ ║
║ - BashPanda 🐼 ║
║ ║
╚═══════════════════════════════════════════════════════════╝
EOF
echo ""
echo "🥋 DEINE REISE DURCH DAS DOJO:"
echo ""
echo " 🖤 Schwarzer Gürtel: echo, Variablen, read"
echo " 💖 Pinker Gürtel: if/then, Schleifen, Arrays"
echo " 💙 Blauer Gürtel: sed, case, bc"
echo " 💚 Grüner Gürtel: grep, regex, Pattern"
echo " 💛 Gelber Gürtel: Funktionen, source"
echo " 🤍 Weisser Gürtel: Background Jobs, Prozesse"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🌲 WILLKOMMEN IN DER CRUMBCREW! 🌲"
echo ""
echo " Du kannst nun:"
echo " - Alle 18 Waldwächter befehligen"
echo " - Eigene Bash-Skripte meistern"
echo " - Komplexe Missions im Crumbforest lösen"
echo ""
echo "🎯 Nächste Schritte:"
echo " - Erkunde missions/robots/ (Hardware-Projekte)"
echo " - Nutze ./crumb-mission-selector.sh (Option 9: CrumbCrew)"
echo " - Erstelle eigene Skripte und teile sie!"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🐼 BashPanda sagt zum Abschied:"
echo ""
bashpanda "Was ist die Weisheit eines Bash-Meisters?"
else
cat << 'EOF'
🐼 "Der weisse Gürtel ist der Anfang, nicht das Ende.
Übe weiter. Der Weg des Meisters ist niemals vollendet."
EOF
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💬 bashpanda \"Was sind Background-Prozesse?\""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

138
missions/evaluate_mission_data.sh Executable file
View File

@@ -0,0 +1,138 @@
#!/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 $?
elif [[ "$MISSION_ID" == "solar_kettle" ]]; then
# --> Rerouting zur Solar Mission
echo ">> Routing zu: Solar Wasserkocher (Tobi Physics)"
echo "$INPUT_DATA" | "${MISSION_DIR}/robots/evaluate_solar_kettle.sh"
exit $?
fi
# ============================================================
# DEFAULT / LEGACY LOGIC (Rainbow Counter)
# ============================================================
# Falls keine Mission-ID gefunden wurde oder unbekannt, gehen wir davon aus,
# dass es der Rainbow Counter ist (Backward Compatibility).
echo ">> Routing zu: Standard (Rainbow Counter)"
echo ""
# Validierung
if [[ "$INPUT_DATA" != *"{"* ]] || [[ "$INPUT_DATA" != *"}"* ]]; then
echo "❌ FEHLER: Das sieht nicht wie gültiges JSON aus."
exit 1
fi
has_jq=$(command -v jq)
# Versuch, die relevante Zeile zu finden (total_events)
VALID_JSON_LINE=$(echo "$INPUT_DATA" | grep '"total_events":' | head -n 1)
if [ -z "$VALID_JSON_LINE" ]; then
CLEAN_JSON="$INPUT_DATA"
else
CLEAN_JSON="$VALID_JSON_LINE"
fi
CLEAN_JSON=$(echo "$CLEAN_JSON" | sed 's/^[^{]*{/{/; s/}[^}]*$/}/')
if [ -n "$has_jq" ] && echo "$CLEAN_JSON" | jq . >/dev/null 2>&1; then
TOTAL=$(echo "$CLEAN_JSON" | jq -r '.total_events // 0')
DOMINANT=$(echo "$CLEAN_JSON" | jq -r '.dominant // "unknown"')
RED=$(echo "$CLEAN_JSON" | jq -r '.classes.red // 0')
BLUE=$(echo "$CLEAN_JSON" | jq -r '.classes.blue // 0')
else
# Fallback Parser
TOTAL=$(echo "$CLEAN_JSON" | grep -o '"total_events": *[0-9]*' | awk -F: '{print $2}' | tr -d ' ,')
DOMINANT=$(echo "$INPUT_DATA" | grep -o '"dominant": *"[^"]*"' | awk -F: '{print $2}' | tr -d ' "')
RED=$(echo "$INPUT_DATA" | grep -o '"red": *[0-9]*' | awk -F: '{print $2}' | tr -d ' ,')
BLUE=$(echo "$INPUT_DATA" | grep -o '"blue": *[0-9]*' | awk -F: '{print $2}' | tr -d ' ,')
fi
# Default Werte falls leer
TOTAL=${TOTAL:-0}
RED=${RED:-0}
BLUE=${BLUE:-0}
DOMINANT=${DOMINANT:-unknown}
# Feedback der Crew
echo "🐘 DumboSQL prüft die Struktur..."
sleep 1
dumbosql "Datensatz empfangen. $TOTAL Ereignisse gefunden. Die Syntax ist valide. Speichere in temporärem Cache..."
echo ""
echo "🦊 FunkFox analysiert den Flow..."
sleep 1
if [ "$TOTAL" -gt 10 ]; then
funkfox "Wow, da ist ordentlich was los! $TOTAL Signale verarbeitet. Der Flow ist fast schon ein Stream!"
elif [ "$TOTAL" -eq 0 ]; then
funkfox "Äh, Stille? Ich höre nichts. Null Events. Sicher, dass der Code lief?"
else
funkfox "Okay, $TOTAL Signale. Ein guter Start für einen kleinen Loop."
fi
echo ""
echo "🦉 Maya-Eule betrachtet die Farben..."
sleep 1
case $DOMINANT in
"red") mayaeule "Rot dominiert. Energie und Warnung." ;;
"blue") mayaeule "Blau ist stark. Ruhe und Technik." ;;
"green") mayaeule "Grün wie der Wald. Alles im Bereich." ;;
"yellow") mayaeule "Gelb leuchtet wie die Sonne." ;;
*) mayaeule "Eine interessante Mischung ($DOMINANT). Vielfalt ist gut." ;;
esac
echo ""
echo "📊 Statistik:"
echo " 🔴 Rot: $RED"
echo " 🔵 Blau: $BLUE"
echo ""
cat << "EOF"
✅ ANALYSE ABGESCHLOSSEN
Die Crew bestätigt: Dein Blockly-Code funktioniert!
EOF

60
missions/evaluate_sign.sh Executable file
View File

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

View File

@@ -0,0 +1,47 @@
#!/bin/bash
# ☀️ Auswertung: Solar Wasserkocher
MISSION_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${MISSION_DIR}/../../lib/waldwaechter.sh"
# Input lesen
INPUT_DATA=$(cat)
# Extrahieren (Fallback grep)
TEMP=$(echo "$INPUT_DATA" | grep -o '"final_temp": *"[^"]*"' | cut -d'"' -f4)
ENERGY=$(echo "$INPUT_DATA" | grep -o '"energy_kj": *"[^"]*"' | cut -d'"' -f4)
TIME=$(echo "$INPUT_DATA" | grep -o '"sim_time": *"[^"]*"' | cut -d'"' -f4)
# Default values
TEMP=${TEMP:-0}
ENERGY=${ENERGY:-0}
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 Messdaten empfangen:"
echo " Temp: ${TEMP}°C"
echo " Energie: ${ENERGY} kJ"
echo " Zeit: ${TIME} s"
echo ""
echo "🐿️ Tobi rechnet nach..."
sleep 1
# Physik Check
# Ziel: 100 Grad.
# Allow 95-105 tolerance (simulation steps)
if (( $(echo "$TEMP >= 95" | bc -l) )) && (( $(echo "$TEMP <= 105" | bc -l) )); then
tobi "Perfekt! Das Wasser kocht ($TEMP°C). Energie effizient genutzt."
echo ""
echo "🦊 FunkFox:"
funkfox "Der Loop war tight! Genau im richtigen Moment den Drop (Break) gesetzt."
elif (( $(echo "$TEMP < 95" | bc -l) )); then
tobi "Zu kalt! ($TEMP°C). Der Tee wird nichts. Du hast zu früh aufgehört zu heizen."
else
tobi "VORSICHT! $TEMP°C! Das ist Dampf unter Druck! Energieverschwendung!"
echo " (Du hast zu lange geheizt. Prüfe deine Abbruchbedingung!)"
fi
echo ""
cat << "EOF"
✅ MISSION STATUS: EVALUIERT
EOF

View File

@@ -0,0 +1,8 @@
{
"title": "Solar Wasserkocher",
"icon": "☀️",
"description": "Lerne Schleifen und Physik: Heize Wasser mit der Kraft der Sonne!",
"difficulty": "medium",
"author": "CapaciTobi",
"enabled": true
}

51
missions/robots/solar_kettle.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/bash
# ☀️ Solar Wasserkocher - Die Physik-Schleife
# Mission: Robots / Algorithms
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Relativer Pfad zur Lib (missions/robots -> ../../lib)
LIB_PATH="${SCRIPT_DIR}/../../lib/waldwaechter.sh"
if [ -f "$LIB_PATH" ]; then
source "$LIB_PATH"
else
echo "⚠️ Waldwächter Lib nicht gefunden."
exit 1
fi
clear
cat << "EOF"
☀️ SOLAR WASSERKOCHER ☀️
Eine Mission über Energie, Schleifen und Variablen.
EOF
echo ""
sleep 1
echo "🐌 Schnecki kriecht herein..."
schnecki "Huhu! Wir haben einen neuen Solar-Kocher. Aber er hat keinen Ausschalter! Er heizt einfach immer weiter, bis er explodiert! Waaaah!"
echo ""
sleep 1
echo "🐿️ (Capaci)Tobi eilt herbei..."
tobi "Ganz ruhig, Schnecki. Physikalisch betrachtet brauchen wir nur einen Regelkreis. Einen Algorithmus."
tobi "Energie rein -> Temperatur hoch -> Prüfen -> Stop."
echo ""
sleep 1
echo "🦊 FunkFox nickt..."
funkfox "Yo, das ist ein Loop! 'While not boiling, keep heating'. Ein klassischer Beat."
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 DEINE MISSION:"
echo "1. Öffne: crumbblocks/solar_kettle_dark.html"
echo " (Nutze './start_crumbblocks.sh', falls Server aus ist)"
echo "2. Baue den Algorithmus:"
echo " - Messe die Temperatur."
echo " - Wenn 'kühl', dann Heizung AN."
echo " - Warte kurz (Physik braucht Zeit!)."
echo " - Wiederhole, bis 100°C erreicht sind."
echo "3. Sende die Daten an die Crew."
echo ""
read -p "🚀 Bereit zum Kochen? (Enter) "

48
release_notes.md Normal file
View File

@@ -0,0 +1,48 @@
# 🌲 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. Neue Robot-Mission: "Solar Wasserkocher"
- **Kategorie:** 🤖 Robots
- **Ziel:** Bringe Wasser zum Kochen, nutze Solar-Energie effizient.
- **Tech-Stack:** Physics Engine (JS) + Blockly Loop-Logic.
- **Features:**
- Echtzeit-Simulation (Sonne, Wolken, Wärme, Energie).
- Asynchrone Loop-Ausführung im Browser ("Non-Blocking").
- **Dark Mint Forest UI:** Augenfreundliches Dark-Theme für lange Sessions 🌙
### 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!"** 🌱

File diff suppressed because it is too large Load Diff

Submodule schnippsi_ui/ttyd deleted from 05422dc91f

48
seeds/README.md Normal file
View File

@@ -0,0 +1,48 @@
# 🌱 Seeds - Wurzeln für den Crumbforest
Dieses Verzeichnis enthält **Seed-Dateien** - Ausgangspunkte für kollaborative Systeme.
## crumb_memo.seed.json
Die erste Wurzel für das **Crumb Memo System** - Output-Transparenz für kreative Krümel.
### Philosophie
*"Ein Baum braucht eine Wurzel. Ein Wald braucht den ersten Baum."*
Diese Datei ist der erste Knoten - erweiterbar von allen.
### Verwendung
```bash
# Im CrumbCrew Shell:
crumb_memo "https://mixcloud.com/deine-url" "Dein kreativer Moment"
crew_memo # Zeige alle Krümel
```
### Struktur
```json
{
"zeit": "2025-12-21 23:00:00",
"link": "https://...",
"typ": "mixcloud|soundcloud|youtube|git",
"notiz": "Deine Notiz"
}
```
## CrewLove 💚
Jeder kann erweitern. Gemeinsam wachsen im Nullfeld der Transparenz.
**Unterstützte Plattformen:**
- 🎧 Mixcloud
- 🔊 SoundCloud
- 📹 YouTube
- 💾 Git (GitHub, GitLab)
**Transparenz in beide Richtungen:**
- Input: `crew_tokens` - Was frage ich?
- Output: `crew_memo` - Was schaffe ich?
*Die Krümel tanzen im Nullfeld!* 🌲✨

View File

@@ -0,0 +1,20 @@
[
{
"zeit": "2025-12-21 23:00:57",
"link": "https://mixcloud.com/digfafunk/",
"typ": "mixcloud",
"notiz": "Digi's Mixcloud Profil - Der erste Krümel im Nullfeld"
},
{
"zeit": "2025-12-21 23:01:04",
"link": "https://github.com/kruemel/crumbmissions",
"typ": "git",
"notiz": "CF_Zero_V1 Repo - Wo die Waldwächter wohnen"
},
{
"zeit": "2025-12-21 23:01:04",
"link": "https://soundcloud.com/digfafunk",
"typ": "soundcloud",
"notiz": "Digi on SoundCloud - Klangwelten"
}
]

71
start_crumbblocks.sh Executable file
View File

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