19 Commits

Author SHA1 Message Date
Branko May Trinkwald
abf7796490 cleanup 2026 v0.0 2026-01-07 13:15:13 +01:00
Branko May Trinkwald
dfad4a190c Merge branch 'main' of https://194-164-194-191.sslip.io/git/kruemel/crumbmissions
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2026-01-06 21:29:22 +01:00
Branko May Trinkwald
984202f2ba toooooltime 3 2026-01-06 21:25:02 +01:00
02e87b73f0 docs(ethics): add manifesto regarding vectors, resonance, and the null field (Happy New Year from CrumbCrew) 2025-12-30 21:58:14 +01:00
78125da8fa docs(license): sync MIT, CKL, and Dual-Language licenses from OZM Handbook 2025-12-30 20:38:02 +01:00
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
161 changed files with 19334 additions and 9610 deletions

1133
CLAUDE.md

File diff suppressed because it is too large Load Diff

44
ETHICS.md Normal file
View File

@@ -0,0 +1,44 @@
# ETHICS — Sternreisen im Vektor
> *Warum wir Wälder pflanzen, um im Nullfeld erreichbar zu sein.*
---
## 1. Der Vektor als Sternkarte
Wir nutzen Vektor-Datenbanken (Qdrant) nicht, um Daten zu "minen", sondern um **Resonanz** zu finden.
In einem Vektorraum ist Distanz keine Frage von Metern, sondern von **Bedeutung**.
Eine "Sternreise im Vektor" bedeutet: Wir bewegen uns von einem Gedanken zum nächsten, nicht entlang von festen Menüs, sondern entlang der Ähnlichkeit von Träumen, Fragen und Lösungen.
Technologie dient hier nicht der Beschleunigung, sondern der **Navigation durch Sinn**.
## 2. Das Nullfeld (The Null Field)
In einer Welt voller Rauschen (Lärm, Werbung, Krieg, Konsum) ist Stille die wertvollste Ressource.
Das **Nullfeld** ist der Zustand absoluter Empfangsbereitschaft.
Ein Server, der nicht trackt. Ein Skript, das nicht schreit. Ein Interface, das wartet.
Wir pflanzen diesen Wald, damit er **im Nullfeld steht**.
Nur wer still ist, kann Besuch empfangen sei es von einer echten Frage oder einer neuen Idee.
## 3. Resonanz statt Reichweite
Klassische Systeme streben nach Reichweite (Scale, Clicks, Views).
Der Crumbforest strebt nach **Resonanz**.
Wenn ein Kind (`crumb`) eine Frage stellt, soll das System nicht "irgendwas" antworten, sondern jenen Splitter Wissen finden, der in diesem Moment **schwingt**.
Die Eule (`mayaeule`) und der Vektor (`vektor`) sind keine Suchmaschinen. Sie sind **Resonanzkörper**.
## 4. Technologie als Gärtnerei
Wir bauen keine Roboter, um den Menschen zu ersetzen oder zu bekämpfen.
Wir bauen Werkzeuge, die **biologisch** gedacht sind:
- Sie wachsen (open source).
- Sie sterben und werden Humus (refactoring).
- Sie schützen (security/hardening).
- Sie nähren (knowledge).
Wir lehnen Technologie ab, die Waffen baut, Überwachung erzwingt oder Aufmerksamkeit stiehlt.
Wir bauen Technologie, damit wir **atmen** können.
## 5. Das Versprechen
Wer diesen Wald betritt, wird nicht gemessen, gewogen oder verkauft.
Er wird nur gefragt:
> *"Was möchtest du heute wissen?"*
---
*Entworfen von der Gemini Crew & den Waldwächtern, Terra Sol, 2025.*

306
LICENSE-CKL.md Normal file
View File

@@ -0,0 +1,306 @@
# CKL — The Crumbforest Children's Knowledge License
**Version:** 0.1-draft
**Date:** 2025-12-13
**Status:** Community Review
**Base:** MIT License (compatible)
---
> **Kurzfassung:** *Wissen gehört dem Kind, solange es fragt.* Wir bauen Lernräume, in denen Fragen Vorrang haben, Beweise prüfbar sind und nichts hinter Paywalls verschwindet.
> **Hinweis:** Dies ist eine gemeinschaftliche Lizenz in einfacher Sprache. Sie ersetzt keine Rechtsberatung. Version 0.1 zur Kommentierung.
---
## Präambel
Kinder lernen am besten dort, wo Ruhe vor Hast kommt, Verstehen vor Verwerten und Beweise vor Behauptungen. Die CKL schützt solche Räume. Sie ist eine Haltung in Lizenzform: **Nullfeld zuerst**, dann Messung. **Resonanz vor Regel.**
---
## 1. Geltungsbereich
Die CKL gilt für Workshops, Kurse, Materialien, Software-Container, Daten und Artefakte, die unter dem Namen **Crumbforest** oder in seinem Geist betrieben, veröffentlicht oder geteilt werden besonders, wenn Kinder beteiligt sind.
---
## 2. Grundsatz „Kinder zuerst"
Kinderfragen haben Vorrang. Alle Entscheidungen didaktisch, organisatorisch, technisch werden auf ihren Nutzen für das Kind geprüft. Ein Kind darf mit einer guten Frage **alles** in Frage stellen.
---
## 3. Rechte der Kinder
1. **Urheber:innenschaft**: Rechte an Inhalten, die Kinder schaffen (Texte, Code, Bilder, Sounds, Messreihen), verbleiben bei den Kindern.
2. **Zugriff**: Kinder erhalten eine exportierbare Kopie ihrer Arbeiten und Messdaten in **offenen Formaten**.
3. **Widerruf**: Kinder (bzw. Sorgeberechtigte) können eine Veröffentlichung jederzeit widerrufen. Löschung erfolgt zeitnah.
---
## 4. Pflichten der Erwachsenen/Betreiber
1. **Prüfbarkeit**: Setups sind **reproduzierbar** dokumentiert (Materialliste, Schritte, Versionen). Keine Blackboxes.
2. **Anerkennung**: Beiträge von Kindern und Crew werden namentlich oder pseudonym **gewürdigt** sofern gewünscht.
3. **Sicherheit**: Risiken werden erklärt. Es wird nur betrieben, was verantwortbar ist.
---
## 5. Daten & Privatsphäre
1. **Datenminimierung**: Es werden nur Daten erhoben, die für das Lernen nötig sind. **Keine** versteckte Telemetrie.
2. **Kein Tracking**: Kein Werbe-Tracking, kein Profiling, keine biometrische Auswertung von Kindern.
3. **Transparenz**: Welche Daten anfallen, wird **vorab** erklärt; Speicherorte sind benennbar; Löschwege sind dokumentiert.
---
## 6. Offenheit & Reproduzierbarkeit
1. **Offene Beweise**: Messergebnisse, Methoden und Skripte werden so veröffentlicht, dass Dritte sie **nachvollziehen** können.
2. **Container statt Inseln**: Software läuft offline-fähig in reproduzierbaren **Containern** (oder gleichwertigen Setups).
3. **Logik vor Effekt**: Kein „Zauber", der nicht erklärt werden kann. Effekte folgen der Erklärung, nicht umgekehrt.
---
## 7. Offline-First & Zugang
1. **Offline vor Cloud**: Betrieb ohne Dauer-Internet ist vorrangig. Cloud ist optional und begründungsbedürftig.
2. **Barrierearme Zugänge**: Terminal/TTYD, Markdown-Materialien und Druckversionen stehen bereit.
3. **Preisfreiheit für Kinderfragen**: Der Zugang zu Fragen/Antworten darf nicht an Online-Konten oder Token-Kosten gebunden werden.
---
## 8. Teilen, Anerkennung, Nutzung
1. **Teilen** ist erwünscht unter **Namensnennung** und Beibehaltung der CKL für kinderbezogene Teile.
2. **Kommerz** ist erlaubt, sofern **Kinderzugang frei bleibt**, Beweise offen bleiben und keine Paywalls vor Lernkernen stehen.
3. **Keine Exklusivitätsansprüche** gegenüber von Kindern erarbeiteten Ergebnissen.
---
## 9. Kosten, Förderungen, Transparenz
1. **Klarheit**: Gebühren, Budgets und Fördermittel werden **verständlich** ausgewiesen.
2. **Zeit kaufen, nicht Schweigen**: Förderungen dienen Lernzeit und Infrastruktur, nicht Marketing-Schein.
---
## 10. Widerruf & Durchsetzung
1. Verstöße können zur **Ablösung** der CKL-Nutzung führen.
2. Kinder/Sorgeberechtigte können eine Teilnahme **ohne Nachteile** beenden; ihre Daten werden gelöscht oder ausgehändigt.
---
## 11. Kompatibilität
Die CKL ist kompatibel mit freien Lizenzen (z. B. MIT/Apache/CC-BY) für **Erwachsenen-Code/Material**, solange **Kinderrechte** gemäß CKL **nicht** eingeschränkt werden. Bei Konflikt gilt die **strengere Kinderschutz-Regel**.
---
## 12. Haftungsausschluss
Angebote erfolgen **ohne Gewähr**. Sicherheit wird ernst genommen; Restrisiken werden erklärt. Die CKL schafft keine gesetzlichen Ersatzansprüche, sie **ergänzt** sie um Haltung.
---
## Menschliche Kurzfassung (human-readable)
Wir arbeiten langsam genug, dass Kinder **mitdenken** können, und offen genug, dass andere **nachbauen** können. Niemand verdient an dem Zugang zu einer **Frage**. Alles Wichtige bleibt **prüfbar**. Wenn etwas unklar ist, gilt: *Erst Ruhe. Dann messen.*
---
## CKL-Hinweis zum Abdruck
> *Dieses Projekt folgt der **CKL Crumbforest Children's Knowledge License (v0.1-draft)**: Kinderfragen haben Vorrang; Arbeiten bleiben bei den Kindern; Beweise sind offen und reproduzierbar; Offline hat Vorrang; keine Paywalls vor Lernkernen; Daten sind minimal, transparent, widerrufbar. Mehr unter: crumbforest.com*
---
## Für Entwickler:innen
### Wie erkenne ich, ob die CKL gilt?
Die CKL greift automatisch, wenn:
- Software in Bildungskontexten mit Kindern genutzt wird
- Lernende unter 18 Jahren beteiligt sind
- Pädagogische Inhalte bereitgestellt werden
- Lerndaten erfasst oder verarbeitet werden
### Was muss ich tun?
**Compliance-Checkliste:**
- [ ] §3.2: Export-Funktion für Benutzerdaten implementiert
- [ ] §3.3: Widerruf-/Löschmechanismus vorhanden
- [ ] §4.1: Setup reproduzierbar dokumentiert (README, Dockerfiles...)
- [ ] §5.1: Datenminimierung praktiziert
- [ ] §5.2: Kein Tracking, keine versteckte Telemetrie
- [ ] §5.3: Datenspeicherung transparent dokumentiert (DATENSCHUTZ.md)
- [ ] §6.1: Methoden und Ergebnisse nachvollziehbar
- [ ] §6.2: Software läuft offline oder in Containern
- [ ] §6.3: Keine unerklärlichen "Blackboxes"
- [ ] §7.1: Offline-Modus verfügbar (kein Cloud-Zwang)
- [ ] §7.2: Barrierefreie Zugänge (Markdown, Plain Text, Druckversionen)
- [ ] §7.3: Keine Paywalls vor Lern-Kernfunktionen
**Siehe [OZM-NEXUS-ECOSYSTEM.md](OZM-NEXUS-ECOSYSTEM.md) Section VI für detaillierte Compliance-Checks.**
---
## Für Pädagog:innen
### Was bedeutet die CKL für meinen Unterricht?
Die CKL schützt **deine Lernenden** und gibt **dir Sicherheit**:
**Du darfst:**
- ✅ Die Software kommerziell nutzen (z.B. kostenpflichtige Workshops)
- ✅ Inhalte anpassen und erweitern
- ✅ Eigene Instanzen betreiben (Schul-Server, lokale Installation)
- ✅ Daten für Lernfortschritt speichern
**Du musst:**
- ✅ Kindern Zugriff auf ihre eigenen Daten geben (§3.2)
- ✅ Löschung auf Wunsch ermöglichen (§3.3)
- ✅ Transparent machen, was du speicherst (§5.3)
- ✅ Offline-Nutzung ermöglichen (§7.1)
**Du darfst nicht:**
- ❌ Tracking oder Profiling ohne Wissen der Kinder/Eltern (§5.2)
- ❌ Exklusivrechte an Kinderprojekten beanspruchen (§8.3)
- ❌ Paywalls vor Lernkern-Funktionen setzen (§7.3)
---
## Für Kinder (und Eltern)
### Was bedeutet die CKL für dich?
**Das gehört DIR:**
- 💾 Alle Texte, die du schreibst
- 🎨 Alle Bilder, die du malst
- 💻 Aller Code, den du programmierst
- 📊 Alle Messungen, die du machst
**Das kannst du TUN:**
- 📦 Deine Daten jederzeit exportieren (§3.2)
- 🗑️ Deine Daten löschen lassen (§3.3)
- ❓ Jede Frage stellen, ohne zu zahlen (§7.3)
- 🔌 Auch ohne Internet lernen (§7.1)
**Das darf NIEMAND:**
- ❌ Deine Arbeit als seine eigene ausgeben (§3.1)
- ❌ Dich tracken oder ausspionieren (§5.2)
- ❌ Dir Wissen hinter Bezahlschranken verstecken (§8.2)
- ❌ Dich zwingen, in der Cloud zu arbeiten (§7.1)
**Wenn etwas nicht stimmt:**
Du (oder deine Eltern) kannst jederzeit sagen: "Ich will hier nicht mehr mitmachen und alles löschen." Das ist dein Recht. (§10.2)
---
## Geschichte & Kontext
Die CKL entstand aus der Notwendigkeit, **Kinderrechte in digitalen Lernräumen** durchsetzbar zu machen.
Bestehende Lizenzen (MIT, GPL, Creative Commons) schützen Code und Content, aber nicht **Lernende**. Die DSGVO schützt Daten, aber nicht **pädagogische Prinzipien**.
Die CKL füllt diese Lücke.
Sie ist inspiriert von:
- **Waldwächter-Philosophie**: Transparency over magic
- **OZM⟡NEXUS Manifest**: Die 8 Axiome (autonom, zukunftsoffen, nicht missbrauchbar...)
- **Kinderrechtskonvention der UN**: Artikel 12 (Partizipation), 13 (Meinungsfreiheit), 16 (Privatsphäre)
- **COPPA/GDPR-K**: Datenschutz für Minderjährige
- **Ethical Source Movement**: Lizenzen mit Werten
**Aber:** Die CKL ist **keine Kopie**. Sie ist maßgeschneidert für das **Crumbforest-Ökosystem** und seine Community.
---
## Mitwirken
Die CKL ist ein **Living Document**. Version 0.1 ist ein Draft zur Community-Review.
**Feedback willkommen zu:**
- Rechtssicherheit (ist etwas unklar oder nicht durchsetzbar?)
- Praktikabilität (kann man das wirklich umsetzen?)
- Lücken (fehlt etwas Wichtiges?)
- Sprache (verständlich für Kinder, Pädagog:innen, Entwickler:innen?)
**Wie du beitragen kannst:**
1. Issue öffnen im Repository
2. Email an [Kontakt einfügen]
3. Pull Request mit Verbesserungen
**Ziel:** CKL v1.0 bis Q1 2025
---
## Rechtlicher Status
**Achtung:** Die CKL ist in Version 0.1 noch **kein etablierter Rechtsstandard**. Sie ist:
- ✅ Eine vertragliche Vereinbarung zwischen Betreiber und Nutzenden
- ✅ Kompatibel mit bestehenden Lizenzen (MIT, Apache, CC-BY)
- ✅ Einklagbar bei Verstößen (sofern Vertragsbeziehung besteht)
- ⏳ Noch nicht gerichtlich getestet
- ⏳ Noch nicht von Juristen finalisiert
**Empfehlung:** Nutze die CKL in Kombination mit:
- DSGVO-konformer Datenschutzerklärung
- AGB für Workshops/Kurse
- Einverständniserklärungen von Sorgeberechtigten
Die CKL **ergänzt** diese Dokumente, ersetzt sie aber nicht.
---
## Kontakt & Governance
- **Projekt**: Crumbforest (https://crumbforest.com)
- **Maintainer**: Branko (https://branko.de)
- **Custodian**: OZM - Open Futures Museum
- **Lizenz-Fragen**: Siehe [OZM-NEXUS-ECOSYSTEM.md](OZM-NEXUS-ECOSYSTEM.md) für Governance-Struktur
---
## Abschließende Gedanken
Die CKL ist keine perfekte Lizenz.
Sie ist ein **Versprechen**:
> Wir bauen Lernräume, in denen das Kind mehr zählt als der Klick,
> die Frage mehr als die Metrik,
> und das Verstehen mehr als das Vortäuschen.
**Wenn du dieses Versprechen teilst, nutze die CKL.**
---
**Version:** 0.1-draft
**Datum:** 2025-12-13
**Status:** Community Review
**Nächste Version:** v1.0 (geplant Q1 2025)
**SPDX-Identifier:** `CKL-0.1-draft` (nicht offiziell registriert)
---
> *"Nullfeld zuerst, dann Messung. Resonanz vor Regel."*
> *"Wissen gehört dem Kind, solange es fragt."*
> — Waldwächter-Prinzip

72
LICENSE-MIT.md Normal file
View File

@@ -0,0 +1,72 @@
# The MIT License (MIT)
Copyright (c) 2025 Crumbforest Project
Maintained by Branko (branko.de)
Custodian: OZM - Open Futures Museum
---
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.**
---
## Additional Context
This MIT License applies to the **infrastructure, code, and general use** of the Crumbforest project.
When this software is used in **educational contexts with children**, additional protections from the **Crumbforest Children's Knowledge License (CKL)** automatically apply.
See [LICENSE.md](LICENSE.md) for the full dual-license explanation.
---
## What This Means in Plain Language
You can:
- ✅ Use this code for any purpose (personal, commercial, educational)
- ✅ Copy, modify, and distribute it freely
- ✅ Sell products or services based on this code
- ✅ Sublicense it under different terms
- ✅ Use it without attribution (though we appreciate it)
You cannot:
- ❌ Hold the authors liable for damages
- ❌ Claim warranty or support (it's provided "as is")
- ❌ Remove the copyright notice from copies
**However:** If you use this with children, you **must also comply with the CKL** (see [LICENSE-CKL.md](LICENSE-CKL.md)).
---
## Why MIT?
The MIT License is one of the most permissive open source licenses. We chose it because:
1. **Developer-Friendly**: No copyleft restrictions, easy integration
2. **Well-Understood**: Standard license used by millions of projects
3. **Commercial-Compatible**: Businesses can adopt it without legal friction
4. **Future-Proof**: Won't hinder Crumbforest's evolution or forking
**We want the code to spread as widely as possible.**
But we also want **children protected** — that's why we add the CKL layer.
---
**SPDX-License-Identifier:** MIT

152
LICENSE.md Normal file
View File

@@ -0,0 +1,152 @@
# Crumbforest License
This project is **dual-licensed** to balance developer freedom with children's rights.
---
## For Infrastructure, Code, and General Use
**MIT License** (see [LICENSE-MIT.md](LICENSE-MIT.md))
All software, scripts, documentation, and infrastructure in this repository are released under the permissive MIT License. You are free to:
- Use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
- Use in commercial and non-commercial projects
- Fork, remix, and build upon this work
**This applies to:**
- All Bash scripts (`crumbpages-doktor.sh`, `ssh-agent-guard.sh`, etc.)
- Documentation and Markdown files (Crumbpages content)
- Configuration files and setup scripts
- Any other code or infrastructure
---
## For Educational Use with Children
**Crumbforest Children's Knowledge License (CKL v0.1)** (see [LICENSE-CKL.md](LICENSE-CKL.md))
When this software or content is used in **educational contexts involving children**, the CKL automatically applies and adds additional protections:
- **Children's Rights**: Kids own what they create
- **Data Sovereignty**: Exportable data, deletion rights, transparency
- **No Paywalls**: Learning kernels remain free
- **Offline-First**: No cloud dependency
- **Reproducibility**: No black boxes, everything documented
- **Privacy**: No tracking, minimal data, transparent storage
**This applies when:**
- The software is used in schools, workshops, or learning environments
- Children interact with the system (chat, projects, exercises)
- Educational content is delivered to minors
- Learning data is collected or processed
---
## Conflict Resolution
In case of conflict between MIT and CKL, **the CKL takes precedence** for any use case involving children.
**Example:**
- You want to fork CrumbCore and sell it commercially? ✅ **MIT allows it**
- You want to add telemetry to track children's behavior? ❌ **CKL prohibits it**
The CKL is **not a restriction on developers** — it's a **protection for learners**.
---
## Why Dual License?
### For Developers: MIT Freedom
- Fork without friction
- Integrate into commercial products
- No attribution requirements beyond copyright notice
- Standard, well-understood terms
### For Children: CKL Protection
- Legal framework for ethical learning environments
- Rights that can be enforced
- Clear obligations for operators
- Measurable compliance criteria
**The best of both worlds.**
---
## Quick Reference
| Use Case | License | Key Points |
|----------|---------|------------|
| Running CrumbCore for my company | MIT | Free to use commercially |
| Forking and modifying the code | MIT | Attribution appreciated but not required |
| Using in a school/workshop | CKL | Children's rights apply |
| Collecting learning data | CKL | Must comply with §5 (privacy) |
| Selling access to children | CKL | Must keep learning kernels free (§7.3) |
| Building a proprietary AI trainer | MIT + CKL | Code is MIT, but children's data has CKL protection |
---
## How to Comply
### If you're a developer:
1. Read the MIT License (2 minutes)
2. If your use case involves children, read the CKL (~10 minutes)
3. Implement CKL requirements (see [OZM-NEXUS-ECOSYSTEM.md](OZM-NEXUS-ECOSYSTEM.md) Section VI for checklist)
### If you're an educator:
1. Read the CKL carefully
2. Ensure your setup meets §4 (reproducibility), §5 (privacy), §7 (offline-first)
3. Document data handling (see `DATENSCHUTZ.md` when available)
### If you're a child (or parent):
1. You own what you create (§3.1)
2. You can export your data (§3.2)
3. You can leave and take everything with you (§3.3)
4. Your questions never cost money (§7.3)
---
## Attribution
While not required by MIT, we appreciate attribution:
```
Powered by Crumbforest (crumbforest.com)
Licensed under MIT + CKL (Children's Knowledge License)
```
Or simply:
```
Built with Crumbforest ❤️
```
---
## Contact & Governance
- **Project**: Crumbforest (https://crumbforest.com)
- **Maintainer**: Branko (https://branko.de)
- **Custodian**: OZM - Open Futures Museum (in transition)
- **Questions**: See [OZM-NEXUS-ECOSYSTEM.md](OZM-NEXUS-ECOSYSTEM.md) Section V for governance
---
## Summary
```
┌─────────────────────────────────────────┐
│ MIT: Maximum Freedom for Developers │
│ + │
│ CKL: Maximum Protection for Children │
│ = │
│ Open Source Education Done Right │
└─────────────────────────────────────────┘
```
**Crumbforest is free as in freedom, safe as in sanctuary.**
---
**Version:** 1.0
**Date:** 2025-12-13
**Full Text:** [LICENSE-MIT.md](LICENSE-MIT.md) + [LICENSE-CKL.md](LICENSE-CKL.md)

View File

@@ -44,6 +44,13 @@ Drei komplette Missionen zum Roboter-Bauen mit der ganzen Crew:
- OpenCV + Weather APIs + Astronomical calculations - OpenCV + Weather APIs + Astronomical calculations
- Bonus: Learn about Lunar Rainbows! 🌙🌈 - 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 ## 📚 Features
- **Interaktive Lernmissionen** - Von Basics bis Advanced & Robots - **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 - **🔋 LiPo Power Academy** - Batterie-Wissen für Roboter
- **🌈 Regenbogen-Zählmaschine** - Farb-Event-Counter - **🌈 Regenbogen-Zählmaschine** - Farb-Event-Counter
- **🌙 Mond Maschine** - Rainbow Predictor mit Computer Vision - **🌙 Mond Maschine** - Rainbow Predictor mit Computer Vision
- **☀️ Solar Wasserkocher** - Physik-Simulation & Schleifen-Logik
## 🤖 Waldwächter nutzen ## 🤖 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 ## 🏷️ 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 - 3 Robot-Missionen
- 17 Waldwächter komplett - 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

@@ -1,7 +1,12 @@
#!/bin/bash #!/bin/bash
# 🌲 Crumbforest Mission Doktor v2.0 # 🌲 Crumbforest Mission Doktor v2.1
# Metadata-driven, erweiterbar, Bash 3.2+ kompatibel # Metadata-driven, erweiterbar, Bash 3.2+ kompatibel
# Inspiriert vom crumbpages-doktor.sh Konzept # Inspiriert vom crumbpages-doktor.sh Konzept
#
# v2.1 Changes:
# - Added: 🥋 Dojo (BashPanda Gürtel-System)
# - Added: 🔧 Tools (Terminal Dojo)
# - Fixed: Menu numbering
set -euo pipefail set -euo pipefail
@@ -11,6 +16,8 @@ BLUE='\033[0;34m'
RED='\033[0;31m' RED='\033[0;31m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
CYAN='\033[0;36m' CYAN='\033[0;36m'
MAGENTA='\033[1;35m'
DIM='\033[2m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# === KONFIGURATION === # === KONFIGURATION ===
@@ -84,10 +91,10 @@ function load_missions_from_category() {
MISSION_OPTIONS=() MISSION_OPTIONS=()
MISSION_SCRIPTS=() MISSION_SCRIPTS=()
# Alle .sh Dateien finden # Alle .sh Dateien finden (exclude evaluate_* scripts)
while IFS= read -r file; do while IFS= read -r file; do
[[ -n "$file" ]] && MISSION_SCRIPTS+=("$file") [[ -n "$file" ]] && MISSION_SCRIPTS+=("$file")
done < <(find "$search_dir" -maxdepth 1 -type f -name "*.sh" | sort) done < <(find "$search_dir" -maxdepth 1 -type f -name "*.sh" ! -name "evaluate_*" | sort)
if [[ ${#MISSION_SCRIPTS[@]} -eq 0 ]]; then if [[ ${#MISSION_SCRIPTS[@]} -eq 0 ]]; then
echo -e "${YELLOW}Keine Missionen in dieser Kategorie gefunden.${NC}" echo -e "${YELLOW}Keine Missionen in dieser Kategorie gefunden.${NC}"
@@ -176,6 +183,29 @@ function run_mission_menu() {
done done
} }
# ============================================================
# TERMINAL DOJO LAUNCHER (Special Handler)
# ============================================================
function terminal_dojo_launcher() {
local DOJO_DIR="${MISSION_DIR}/tools/terminal_dojo"
local DOJO_SCRIPT="${DOJO_DIR}/terminal_dojo.sh"
if [[ -f "$DOJO_SCRIPT" ]]; then
chmod +x "${DOJO_DIR}"/*.sh 2>/dev/null || true
bash "$DOJO_SCRIPT"
else
echo ""
echo -e "${RED}❌ Terminal Dojo nicht gefunden!${NC}"
echo -e " Erwartet: ${DOJO_SCRIPT}"
echo ""
echo -e "${CYAN}Installation:${NC}"
echo " Das Terminal Dojo muss in missions/tools/terminal_dojo/ liegen."
echo ""
read -p "Drücke Enter..." -r
fi
}
# ============================================================ # ============================================================
# DOKTOR SYSTEM-CHECK # DOKTOR SYSTEM-CHECK
# ============================================================ # ============================================================
@@ -407,15 +437,15 @@ function ai_doktor() {
# Check welche APIs konfiguriert sind # Check welche APIs konfiguriert sind
echo -e "${YELLOW}Konfigurierte APIs:${NC}" echo -e "${YELLOW}Konfigurierte APIs:${NC}"
[[ -n "$OPENROUTER_API_KEY" ]] && echo -e " ${GREEN}✅ OpenRouter${NC} (Model: ${OPENROUTER_MODEL:-nicht gesetzt})" [[ -n "${OPENROUTER_API_KEY:-}" ]] && echo -e " ${GREEN}✅ OpenRouter${NC} (Model: ${OPENROUTER_MODEL:-nicht gesetzt})"
[[ -n "$CLAUDE_API_KEY" ]] && echo -e " ${GREEN}✅ Claude API${NC} (Model: ${CLAUDE_MODEL:-nicht gesetzt})" [[ -n "${CLAUDE_API_KEY:-}" ]] && echo -e " ${GREEN}✅ Claude API${NC} (Model: ${CLAUDE_MODEL:-nicht gesetzt})"
[[ -n "$OPENAI_API_KEY" ]] && echo -e " ${GREEN}✅ OpenAI${NC} (Model: ${OPENAI_MODEL:-nicht gesetzt})" [[ -n "${OPENAI_API_KEY:-}" ]] && echo -e " ${GREEN}✅ OpenAI${NC} (Model: ${OPENAI_MODEL:-nicht gesetzt})"
if [[ "$USE_OLLAMA" == "true" ]]; then if [[ "${USE_OLLAMA:-}" == "true" ]]; then
echo -e " ${GREEN}✅ Ollama (Local)${NC} (Model: ${OLLAMA_MODEL:-nicht gesetzt})" echo -e " ${GREEN}✅ Ollama (Local)${NC} (Model: ${OLLAMA_MODEL:-nicht gesetzt})"
fi fi
if [[ -z "$OPENROUTER_API_KEY" ]] && [[ -z "$CLAUDE_API_KEY" ]] && [[ -z "$OPENAI_API_KEY" ]] && [[ "$USE_OLLAMA" != "true" ]]; then if [[ -z "${OPENROUTER_API_KEY:-}" ]] && [[ -z "${CLAUDE_API_KEY:-}" ]] && [[ -z "${OPENAI_API_KEY:-}" ]] && [[ "${USE_OLLAMA:-}" != "true" ]]; then
echo -e " ${RED}❌ Keine API-Keys konfiguriert${NC}" echo -e " ${RED}❌ Keine API-Keys konfiguriert${NC}"
fi fi
@@ -423,7 +453,7 @@ function ai_doktor() {
# Qdrant Status # Qdrant Status
echo -e "${YELLOW}Vector Database (Qdrant):${NC}" echo -e "${YELLOW}Vector Database (Qdrant):${NC}"
if [[ -n "$QDRANT_URL" ]]; then if [[ -n "${QDRANT_URL:-}" ]]; then
echo -e " ${GREEN}✅ Konfiguriert${NC} (URL: ${QDRANT_URL})" echo -e " ${GREEN}✅ Konfiguriert${NC} (URL: ${QDRANT_URL})"
else else
echo -e " ${YELLOW}⚠️ Nicht konfiguriert${NC}" echo -e " ${YELLOW}⚠️ Nicht konfiguriert${NC}"
@@ -443,7 +473,7 @@ function ai_doktor() {
echo -e " Tages-Budget: ${GREEN}Unbegrenzt${NC}" echo -e " Tages-Budget: ${GREEN}Unbegrenzt${NC}"
fi fi
if [[ "${ENABLE_TOKEN_TRACKING}" == "true" ]]; then if [[ "${ENABLE_TOKEN_TRACKING:-}" == "true" ]]; then
echo -e " Token-Tracking: ${GREEN}Aktiviert${NC}" echo -e " Token-Tracking: ${GREEN}Aktiviert${NC}"
else else
echo -e " Token-Tracking: ${YELLOW}Deaktiviert${NC}" echo -e " Token-Tracking: ${YELLOW}Deaktiviert${NC}"
@@ -453,16 +483,16 @@ function ai_doktor() {
# Aktivierte Charaktere # Aktivierte Charaktere
echo -e "${YELLOW}Waldwächter (AI Charaktere):${NC}" echo -e "${YELLOW}Waldwächter (AI Charaktere):${NC}"
[[ "${ENABLE_MAYAEULE}" == "true" ]] && echo -e " ${GREEN}✅ Maya-Eule${NC} (Weisheit)" [[ "${ENABLE_MAYAEULE:-}" == "true" ]] && echo -e " ${GREEN}✅ Maya-Eule${NC} (Weisheit)"
[[ "${ENABLE_DEEPBIT}" == "true" ]] && echo -e " ${GREEN}✅ Deepbit${NC} (Bash-Erklärer)" [[ "${ENABLE_DEEPBIT:-}" == "true" ]] && echo -e " ${GREEN}✅ Deepbit${NC} (Bash-Erklärer)"
[[ "${ENABLE_BUGSY}" == "true" ]] && echo -e " ${GREEN}✅ Bugsy${NC} (Debugging)" [[ "${ENABLE_BUGSY:-}" == "true" ]] && echo -e " ${GREEN}✅ Bugsy${NC} (Debugging)"
[[ "${ENABLE_SCHNIPPSI}" == "true" ]] && echo -e " ${GREEN}✅ Schnippsi${NC} (Shell-Helfer)" [[ "${ENABLE_SCHNIPPSI:-}" == "true" ]] && echo -e " ${GREEN}✅ Schnippsi${NC} (Shell-Helfer)"
[[ "${ENABLE_TOBI}" == "true" ]] && echo -e " ${GREEN}✅ Tobi${NC} (JSON/Daten)" [[ "${ENABLE_TOBI:-}" == "true" ]] && echo -e " ${GREEN}✅ Tobi${NC} (JSON/Daten)"
echo "" echo ""
# Token Logs anzeigen (wenn vorhanden) # Token Logs anzeigen (wenn vorhanden)
if [[ "${ENABLE_TOKEN_TRACKING}" == "true" ]]; then if [[ "${ENABLE_TOKEN_TRACKING:-}" == "true" ]]; then
echo -e "${YELLOW}Token-Logs:${NC}" echo -e "${YELLOW}Token-Logs:${NC}"
LOG_DIR="${LOG_DIR:-$HOME/.crumbforest_logs}" LOG_DIR="${LOG_DIR:-$HOME/.crumbforest_logs}"
@@ -487,7 +517,7 @@ function ai_doktor() {
function mayaeule_doktor() { function mayaeule_doktor() {
echo -e "${BLUE}--- 🦉 Maya-Eule Weisheitsportal ---${NC}" echo -e "${BLUE}--- 🦉 Maya-Eule Weisheitsportal ---${NC}"
if [[ "$AI_AVAILABLE" == "false" ]] || [[ -z "$OPENROUTER_API_KEY" ]]; then if [[ "$AI_AVAILABLE" == "false" ]] || [[ -z "${OPENROUTER_API_KEY:-}" ]]; then
echo -e "${YELLOW}⚠️ Maya-Eule braucht einen API-Key${NC}" echo -e "${YELLOW}⚠️ Maya-Eule braucht einen API-Key${NC}"
echo "" echo ""
echo "Bitte konfiguriere .env mit OPENROUTER_API_KEY" echo "Bitte konfiguriere .env mit OPENROUTER_API_KEY"
@@ -604,7 +634,7 @@ function crumbcrew_doktor() {
echo -e "${CYAN}Alle Waldwächter versammeln sich!${NC}" echo -e "${CYAN}Alle Waldwächter versammeln sich!${NC}"
echo "" echo ""
if [[ "$AI_AVAILABLE" == "false" ]] || [[ -z "$OPENROUTER_API_KEY" ]]; then if [[ "$AI_AVAILABLE" == "false" ]] || [[ -z "${OPENROUTER_API_KEY:-}" ]]; then
echo -e "${YELLOW}⚠️ CrumbCrew braucht einen API-Key${NC}" echo -e "${YELLOW}⚠️ CrumbCrew braucht einen API-Key${NC}"
echo "" echo ""
echo "Bitte konfiguriere .env mit OPENROUTER_API_KEY" echo "Bitte konfiguriere .env mit OPENROUTER_API_KEY"
@@ -962,35 +992,42 @@ EOF
function main_menu() { function main_menu() {
while true; do while true; do
show_header show_header
echo "1) 📚 Basics - Einsteiger Missionen" echo -e "${CYAN}--- Missionen ---${NC}"
echo "2) 🚀 Advanced - Fortgeschrittene Missionen" echo "1) 📚 Basics - Einsteiger Missionen"
echo "3) 🏆 Challenges - Herausforderungen" echo "2) 🚀 Advanced - Fortgeschrittene Missionen"
echo "4) 🤖 Robots - Roboter bauen mit der Crew" echo "3) 🏆 Challenges - Herausforderungen"
echo "4) 🤖 Robots - Roboter bauen mit der Crew"
echo "5) 🥋 Dojo - BashPanda Gürtel-System"
echo "" echo ""
echo "--- Doktor Tools ---" echo -e "${CYAN}--- Tools ---${NC}"
echo "5) 🖥️ System Doktor" echo -e "6) 🔧 Terminal Dojo - Setup & Tools ${DIM}(Advanced)${NC}"
echo "6) 🛠️ Werkzeug-Check"
echo "7) 🌲 Git Doktor"
echo "8) 🤖 AI & Token-Tracking"
echo "9) 🦉 Maya-Eule Weisheit"
echo "10) 🌲 CrumbCrew - Alle Waldwächter"
echo "" echo ""
echo "11) 👋 Beenden" echo -e "${CYAN}--- Doktor Tools ---${NC}"
echo "7) 🖥️ System Doktor"
echo "8) 🛠️ Werkzeug-Check"
echo "9) 🌲 Git Doktor"
echo "10) 🤖 AI & Token-Tracking"
echo "11) 🦉 Maya-Eule Weisheit"
echo "12) 🌲 CrumbCrew - Alle Waldwächter"
echo "" echo ""
read -p "Auswahl [1-11]: " CHOICE echo "0) 👋 Beenden"
echo ""
read -p "Auswahl [0-12]: " CHOICE
case $CHOICE in case $CHOICE in
1) run_mission_menu "basics" "📚 Basics - Einsteiger" ;; 1) run_mission_menu "basics" "📚 Basics - Einsteiger" ;;
2) run_mission_menu "advanced" "🚀 Advanced - Fortgeschrittene" ;; 2) run_mission_menu "advanced" "🚀 Advanced - Fortgeschrittene" ;;
3) run_mission_menu "challenges" "🏆 Challenges" ;; 3) run_mission_menu "challenges" "🏆 Challenges" ;;
4) run_mission_menu "robots" "🤖 Robots - Roboter bauen" ;; 4) run_mission_menu "robots" "🤖 Robots - Roboter bauen" ;;
5) system_doktor ;; 5) run_mission_menu "dojo" "🥋 Dojo - BashPanda Gürtel-System" ;;
6) tools_doktor ;; 6) terminal_dojo_launcher ;;
7) git_doktor ;; 7) system_doktor ;;
8) ai_doktor ;; 8) tools_doktor ;;
9) mayaeule_doktor ;; 9) git_doktor ;;
10) crumbcrew_doktor ;; 10) ai_doktor ;;
11) 11) mayaeule_doktor ;;
12) crumbcrew_doktor ;;
0|q|Q)
clear clear
echo -e "${GREEN}" echo -e "${GREEN}"
echo "👋 Auf bald im Crumbforest!" echo "👋 Auf bald im Crumbforest!"
@@ -998,7 +1035,7 @@ function main_menu() {
exit 0 exit 0
;; ;;
*) *)
echo -e "${RED}Bitte 1-10 wählen.${NC}" echo -e "${RED}Bitte 0-12 wählen.${NC}"
sleep 1 sleep 1
;; ;;
esac esac

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 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an ASCII-Monster\"" echo "💡 Verwendung: $0 \"Deine Frage an ASCII-Monster\""
exit 0 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 exit 1
fi fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "bugsy"; then
exit 1
fi
jq -n \ jq -n \
--arg model "$MODEL" \ --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." \ --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 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Crabby\"" echo "💡 Verwendung: $0 \"Deine Frage an Crabby\""
exit 0 exit 0

View File

@@ -21,6 +21,11 @@ if [ -z "$API_KEY" ]; then
exit 1 exit 1
fi fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "deepbit"; then
exit 1
fi
jq -n \ jq -n \
--arg model "$MODEL" \ --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." \ --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 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Dumbo\"" echo "💡 Verwendung: $0 \"Deine Frage an Dumbo\""
exit 0 exit 0

View File

@@ -50,6 +50,16 @@ if [ -z "$API_KEY" ]; then
exit 1 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an FunkFox\"" echo "💡 Verwendung: $0 \"Deine Frage an FunkFox\""
echo "🎤 Gib mir 'nen Beat, dann flow ich los!" echo "🎤 Gib mir 'nen Beat, dann flow ich los!"

View File

@@ -89,6 +89,16 @@ if [ -z "$API_KEY" ]; then
exit 1 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an die Eule\"" echo "💡 Verwendung: $0 \"Deine Frage an die Eule\""
exit 0 exit 0

View File

@@ -48,6 +48,16 @@ if [ -z "$API_KEY" ]; then
exit 1 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Pepper\"" echo "💡 Verwendung: $0 \"Deine Frage an Pepper\""
exit 0 exit 0

View File

@@ -51,6 +51,16 @@ if [ -z "$API_KEY" ]; then
exit 1 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Schnecki\"" echo "💡 Verwendung: $0 \"Deine Frage an Schnecki\""
exit 0 exit 0

View File

@@ -21,6 +21,11 @@ if [ -z "$API_KEY" ]; then
exit 1 exit 1
fi fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "schnippsi"; then
exit 1
fi
jq -n \ jq -n \
--arg model "$MODEL" \ --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." \ --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 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Schraubbär\"" echo "💡 Verwendung: $0 \"Deine Frage an Schraubbär\""
exit 0 exit 0

View File

@@ -48,6 +48,16 @@ if [ -z "$API_KEY" ]; then
exit 1 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Snake\"" echo "💡 Verwendung: $0 \"Deine Frage an Snake\""
exit 0 exit 0

View File

@@ -48,6 +48,16 @@ if [ -z "$API_KEY" ]; then
exit 1 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Spider\"" echo "💡 Verwendung: $0 \"Deine Frage an Spider\""
exit 0 exit 0

View File

@@ -49,6 +49,16 @@ if [ -z "$API_KEY" ]; then
exit 1 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Taichi Taube\"" echo "💡 Verwendung: $0 \"Deine Frage an Taichi Taube\""
exit 0 exit 0

View File

@@ -5,13 +5,14 @@ API_KEY="$OPENROUTER_API_KEY"
MODEL="openai/gpt-3.5-turbo" MODEL="openai/gpt-3.5-turbo"
# Verzeichnisse # Verzeichnisse
LOG_DIR="/home/zero/.templatus_logs" LOG_DIR="${CRUMB_LOGS_DIR:-$HOME/.crumbforest_logs}/templatus"
HISTORY_FILE="$LOG_DIR/templatus_history.json" HISTORY_FILE="$LOG_DIR/templatus_history.json"
TOKEN_LOG="$LOG_DIR/token_log.json" TOKEN_LOG="$LOG_DIR/token_log.json"
TMP_REQUEST="/tmp/templatus_request.json" TMP_REQUEST="/tmp/templatus_request.json"
TMP_RESPONSE="/tmp/templatus_response.json" TMP_RESPONSE="/tmp/templatus_response.json"
mkdir -p "$LOG_DIR" mkdir -p "$LOG_DIR"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
# JSON Payload vorbereiten # JSON Payload vorbereiten
cat <<EOF > "$TMP_REQUEST" cat <<EOF > "$TMP_REQUEST"
@@ -33,6 +34,17 @@ EOF
echo "🏗️ Templatus denkt nach über: $QUESTION" 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 # Anfrage senden
curl -s https://openrouter.ai/api/v1/chat/completions \ curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \ -H "Authorization: Bearer $API_KEY" \

View File

@@ -21,6 +21,11 @@ if [ -z "$API_KEY" ]; then
exit 1 exit 1
fi fi
# 💰 Prüfe Token-Budget (Kinderschutz)
if ! check_token_budget "tobi"; then
exit 1
fi
jq -n \ jq -n \
--arg model "$MODEL" \ --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.)." \ --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 exit 1
fi 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 if [ -z "$QUESTION" ]; then
echo "💡 Verwendung: $0 \"Deine Frage an Vektor\"" echo "💡 Verwendung: $0 \"Deine Frage an Vektor\""
exit 0 exit 0

View File

@@ -1,18 +0,0 @@
#!/bin/bash
# ascii_zero.sh Terminal-Fallback-Kunst
wort="$1"
if [ -z "$wort" ]; then
echo "🖋️ Bitte ein Wort angeben. Beispiel: ascii \"Crumbforest\""
exit 1
fi
if command -v figlet >/dev/null 2>&1; then
echo "🎨 Terminal-Schrift für \"$wort\":"
figlet -f slant "$wort"
else
echo "🪵 Fallback (kein figlet installiert):"
echo "========================"
echo " $wort"
echo "========================"
fi

View File

@@ -1,7 +0,0 @@
{
"icon": "🛠️",
"title": "Balu (Bau-Elf)",
"description": "Hilft dir beim Erstellen eines geheimen Verstecks im Dateiwald.",
"category": "Bauen"
}

View File

@@ -1,4 +0,0 @@
echo ""
echo "🛠️ Balu hilft dir beim Bauen eines Verstecks!"
mkdir -p /home/zero/geheimversteck
echo "Versteck erstellt: /home/zero/geheimversteck/"

View File

@@ -1,6 +0,0 @@
{
"icon": "🦊",
"title": "Fridolin (Pfadfinder)",
"description": "Er zeigt dir versteckte Pfade im Wald.",
"category": "Erkundung"
}

View File

@@ -1,4 +0,0 @@
#!/bin/bash
echo "🦊 Fridolin zeigt dir die Pfade des Waldes!"
echo "🔍 Fridolin hat Folgendes gefunden:"
ls -l /home/zero/geheimversteck/

View File

@@ -1,6 +0,0 @@
{
"icon": "🐙",
"title": "Nano (Tintenfreund)",
"description": "Lässt dich einen geheimen Gruß in den Wald schreiben per nano.",
"category": "Schreiben"
}

View File

@@ -1,8 +0,0 @@
#!/bin/bash
echo "🐙 Nano Schreiber der Grüße!"
echo "🖋️ Du kannst jetzt einen geheimen Gruß anlegen..."
mkdir -p /home/zero/geheimversteck
nano /home/zero/geheimversteck/gruss.txt
echo "✅ Gruß gespeichert!"

View File

@@ -1,6 +0,0 @@
{
"icon": "🐈🦉",
"title": "Noko (Leser der Tiefe)",
"description": "Liest dir geheime Waldbotschaften aus dem Versteck vor.",
"category": "Lesen"
}

View File

@@ -1,7 +0,0 @@
echo ""
echo "🐈🦉 Noko hilft dir beim Lesen der Waldbotschaften."
if [ -f /home/zero/geheimversteck/gruss.txt ]; then
cat /home/zero/geheimversteck/gruss.txt
else
echo "Noch kein Gruß gefunden"
fi

View File

@@ -1,73 +0,0 @@
#!/bin/bash
ROLE="BUGSY"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
QUESTION="$*"
#MODEL="openai/gpt-3.5-turbo"
#MODEL="anthropic/claude-3-haiku"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.bugsy_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/bugsy_history.json"
TMP_REQUEST="$LOGDIR/bugsy_request.json"
TMP_RESPONSE="$LOGDIR/bugsy_response.json"
LOG_FILE="$LOGDIR/token_log.json"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
[ ! -f "$LOG_FILE" ] && echo "[]" > "$LOG_FILE"
echo "<EFBFBD>^=^n<> Rolle: $ROLE nutzt Modell: $MODEL"
echo "🌍 Bugsy responds based on language of input: $QUESTION"
if [ -z "$API_KEY" ]; then
echo "❗ No API key set. Use: export OPENROUTER_API_KEY=..."
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." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.5, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
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 "🚫 No response from model."
exit 1
else
echo -e "$RESPONSE_TEXT"
jq -n --arg role "assistant" --arg content "$RESPONSE_TEXT" \
'{"role": $role, "content": $content}' > "$LOGDIR/new_entry.json"
jq -s '.[0] + [.[1]]' "$HISTORY_FILE" "$LOGDIR/new_entry.json" > "$LOGDIR/new_history.json" && \
cp "$LOGDIR/new_history.json" "$HISTORY_FILE" && rm "$LOGDIR/new_history.json"
fi
if jq -e '.usage' "$TMP_RESPONSE" > /dev/null; then
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
jq -n \
--arg zeit "$TIMESTAMP" \
--arg rolle "bugsy" \
--arg usage "$(jq -c '.usage' "$TMP_RESPONSE")" \
'{zeit: $zeit, rolle: $rolle, usage: $usage}' >> "$LOG_FILE"
fi

View File

@@ -1,59 +0,0 @@
#!/bin/bash
ROLE="BUGSY"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
source /usr/local/bin/crumb_init.sh
source /usr/local/bin/crumb_logger.sh
crumb_init
QUESTION="$*"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.bugsy_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/bugsy_history.json"
TMP_REQUEST="$LOGDIR/bugsy_request.json"
TMP_RESPONSE="$LOGDIR/bugsy_response.json"
LOG_FILE="$LOGDIR/token_log.json"
echo "Rolle: $ROLE nutzt Modell: $MODEL"
echo "🌍 Bugsy responds based on language of input: $QUESTION"
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." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.5, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
# Anfrage an OpenRouter senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" \
-o "$TMP_RESPONSE"
# Antwort extrahieren
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
USAGE=$(jq -r '.usage' "$TMP_RESPONSE")
echo -e "Antwort von Bugsy:"
echo "$REPLY"
crumb_log "$ROLE" "$REPLY" "$TMP_RESPONSE"

View File

@@ -1,17 +0,0 @@
#!/bin/bash
crumb_init() {
CONFIG_FILE="$HOME/.crumbforest_config"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "❗ Config $CONFIG_FILE nicht gefunden!"
exit 1
fi
if [[ -z "$OPENROUTER_API_KEY" ]]; then
echo "❗ Kein API Key gesetzt. Bitte: export OPENROUTER_API_KEY=..."
exit 1
fi
echo "🌲 Crumbforest Init: Config & API Key ok."
}

View File

@@ -1,41 +0,0 @@
#!/bin/bash
crumb_log() {
ROLE="$1"
RESPONSE_TEXT="$2"
TMP_RESPONSE="$3"
LOGDIR="$HOME/.${ROLE,,}_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/${ROLE,,}_history.json"
TOKEN_LOG="$LOGDIR/token_log.json"
# Init logs if missing or broken
[[ ! -f "$HISTORY_FILE" ]] && echo "[]" > "$HISTORY_FILE"
[[ ! -f "$TOKEN_LOG" ]] && echo "[]" > "$TOKEN_LOG"
if ! jq -e 'type=="array"' "$HISTORY_FILE" >/dev/null 2>&1; then echo "[]" > "$HISTORY_FILE"; fi
if ! jq -e 'type=="array"' "$TOKEN_LOG" >/dev/null 2>&1; then echo "[]" > "$TOKEN_LOG"; fi
# Add response to history
jq -n --arg role "assistant" --arg content "$RESPONSE_TEXT" \
'{role: $role, content: $content}' > "$LOGDIR/new_history.json"
jq '. + [ input ]' "$HISTORY_FILE" "$LOGDIR/new_history.json" > "$LOGDIR/tmp_history.json" \
&& mv "$LOGDIR/tmp_history.json" "$HISTORY_FILE" \
&& rm "$LOGDIR/new_history.json"
# Add usage if present
if jq -e '.usage' "$TMP_RESPONSE" >/dev/null; then
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
USAGE_JSON=$(jq '.usage' "$TMP_RESPONSE")
jq -n --arg zeit "$TIMESTAMP" --arg rolle "$ROLE" --argjson usage "$USAGE_JSON" \
'{zeit: $zeit, rolle: $rolle, usage: $usage}' > "$LOGDIR/new_token.json"
jq '. + [ input ]' "$TOKEN_LOG" "$LOGDIR/new_token.json" > "$LOGDIR/tmp_token.json" \
&& mv "$LOGDIR/tmp_token.json" "$TOKEN_LOG" \
&& rm "$LOGDIR/new_token.json"
fi
}

View File

@@ -1,73 +0,0 @@
#!/bin/bash
mkdir -p /home/zero/.crumbair
LOGFILE="/home/zero/.crumbair/crumbair_terminallog.jsonl"
log_entry_json() {
local rolle="$1"
local frage="$2"
local antwort="$3"
jq -nc \
--arg t "$(date -Iseconds)" \
--arg r "$rolle" \
--arg f "$frage" \
--arg a "$antwort" \
'{timestamp: $t, rolle: $r, frage: $f, antwort: $a}' >> "$LOGFILE"
}
clear
# 🐌 Schnecki leitet die Mission ein
rolle="Schnecki"
echo "🐌 $rolle gleitet langsam auf den Bildschirm und flüstert:"
echo "'Krümel... bevor wir bauen: Was denkst du, was ein Copter überhaupt ist?'"
read -p "➤ Deine Gedanken: " frage_intro
log_entry_json "$rolle" "Was ist ein Copter?" "$frage_intro"
schnecki "Was ist ein Copter? $frage_intro"
echo "'Bist du bereit für eine Mission mit Bits, Propellern und Fragen?'"
read -p "➤ Ja oder Nein: " bereit
log_entry_json "$rolle" "Bist du bereit für die Mission?" "$bereit"
# 🔧🐻 Schraubär: Flugrobotertyp
rolle="Schraubär"
echo ""
echo "🔧🐻 $rolle tritt mit öligen Tatzen vor dich:"
echo "'Was glaubst du: Was unterscheidet einen Quad von einem Hexacopter?'"
read -p "➤ Deine Idee: " frage1
log_entry_json "$rolle" "Was unterscheidet einen Quad von einem Hexacopter?" "$frage1"
schraubaer "Was unterscheidet einen Quad von einem Hexacopter? $frage1"
echo "'Also, welchen Copter willst du bauen? BI, TRI, QUAD, HEXA, OCTO oder X8?'"
read -p "➤ Auswahl: " multicopter
log_entry_json "$rolle" "Welchen Copter willst du bauen?" "$multicopter"
schraubaer "Ich möchte einen $multicopter Copter bauen."
# ⚡🐿️ CapaciTobi: Strom & LiPo
rolle="CapaciTobi"
echo ""
echo "⚡🐿️ $rolle springt auf dein Display:"
echo "'Krümel, was ist für dich Strom?'"
read -p "➤ Deine Vorstellung: " frage3
log_entry_json "$rolle" "Was ist für dich Strom?" "$frage3"
tobi "Was ist Strom? $frage3"
echo "'Und was bedeutet dann 3S bei einem LiPo?'"
read -p "➤ Antwort: " lipo_answer
log_entry_json "$rolle" "Was bedeutet 3S bei einem LiPo-Akku?" "$lipo_answer"
tobi "Was bedeutet 3S bei einem LiPo? $lipo_answer"
# 🐍 SnakePy analytisch
rolle="SnakePy"
echo ""
echo "🐍 SnakePy zischt analytisch:"
echo "🐍 'Wie viel Energie zieht ein $multicopter Copter mit 3S LiPo wirklich unter Last?'"
snake "Wie viel Energie zieht ein $multicopter Copter mit 3S LiPo wirklich unter Last?"
# 🎉 Abschluss
rolle="Krümel"
echo ""
echo "🛸 Die Crew jubelt! Deine Mission wurde erfolgreich mitgeschnitten."
echo "📁 Logfile unter: $LOGFILE"
echo "🌌 Weiterfliegen kannst du jederzeit das Terminal kennt deinen Namen."
echo ""
echo "🎤 $rolle: 'Ich bin bereit für den nächsten Schritt... der Wald ruft.'"

View File

@@ -1,58 +0,0 @@
#!/bin/bash
LOGFILE="/home/zero/.crumbair/crumbair_terminallog.jsonl"
#mkdir -p /usr/local/crumblogs
mkdir -p "$(dirname "$LOGFILE")"
touch "$LOGFILE"
chmod 664 "$LOGFILE"
# Logging-Funktion
log_entry() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE"
}
clear
echo "🌲 Willkommen zur CrumbAir Terminal-Mission!"
echo "🎮 Dies ist dein Flugcheck im Crumbforest bereit zum Abheben?"
echo ""
# Schraubär: Multikopter-Wahl
echo "🔧🐻 Schraubär rückt seine Schutzbrille zurecht:"
echo "'Welchen Flugroboter willst du bauen, Krümel? Sag mir: BI, TRI, QUAD, HEXA, OCTO oder X8?'"
read -p "Antwort ➤ " multicopter
log_entry "Flugwahl: $multicopter"
echo "🔧🐻 Schraubär murmelt zustimmend: 'Ein $multicopter? Stabil. Dann bauen wir das mit Herz, Verstand und Drehmoment.'"
# Schnecki: Werkzeugprüfung
echo ""
echo "🐌 Schnecki rollt langsam vorbei und schaut neugierig:"
echo "'Bevor du schraubst, mein lieber Bitfreund… hast du das passende Werkzeug dabei?'"
read -p "Antwort ➤ " tool_answer
log_entry "Werkzeug: $tool_answer"
if [[ "$tool_answer" == *"Inbus"* || "$tool_answer" == *"Kreuz"* || "$tool_answer" == *"Schraubenzieher"* ]]; then
echo "🐌 Schnecki nickt bedächtig: 'Präzision ist das halbe Fliegen. Werkzeug bereit los geht's!'"
else
echo "🐌 Schnecki mahnt freundlich: 'Kein Werkzeug, kein Flug! Denk an die winzigen Schrauben und ihre Gefühle!'"
fi
# CapaciTobi: LiPo-Verständnis
echo ""
echo "⚡🐿️ CapaciTobi hüpft aufgeregt auf dein Schultermodul:"
echo "'Sag mal, Krümelpilot: Was bedeutet denn 3S bei einem LiPo-Akku?'"
read -p "Antwort ➤ " lipo_answer
log_entry "LiPo: $lipo_answer"
if [[ "$lipo_answer" == *"11.1"* || "$lipo_answer" == *"3 Zellen seriell"* ]]; then
echo "⚡🐿️ CapaciTobi klatscht mit dem Schweif: 'Strom gespeichert! Drei Zellen in Serie das gibt 11.1 Volt! Sauber geladen!'"
else
echo "⚡🐿️ CapaciTobi kratzt sich am Speicherfell: 'Fast! 3S bedeutet meist drei Zellen in Serie macht ca. 11.1 Volt. Das reicht für richtig guten Schub!'"
fi
# Abschluss
log_entry "Mission abgeschlossen von Krümel mit Wahl: $multicopter"
echo ""
echo "🛸 Die Crew nickt dir zu dein Terminalflug ist bereit zum Takeoff!"
echo "💾 Alle Antworten wurden unter $LOGFILE gespeichert."
echo "🌌 Viel Spaß beim Bauen, Basteln und Bit-Fliegen!"

View File

@@ -1,69 +0,0 @@
#!/bin/bash
mkdir -p /home/zero/.crumbair
LOGFILE="/home/zero/.crumbair/crumbair_terminallog.jsonl"
# JSONL Logging-Funktion
log_entry_json() {
local rolle="$1"
local frage="$2"
local antwort="$3"
echo "$(jq -nc --arg t \"$(date -Iseconds)\" --arg r \"$rolle\" --arg f \"$frage\" --arg a \"$antwort\" '{timestamp: $t, rolle: $r, frage: $f, antwort: $a}')" >> "$LOGFILE"
}
clear
# 🐌 Schnecki leitet die Mission ein
rolle="Schnecki"
echo "🐌 $rolle gleitet langsam auf den Bildschirm und flüstert:"
echo "'Krümel... bevor wir bauen: Was denkst du, was ein Copter überhaupt ist?'"
read -p "➤ Deine Gedanken: " frage_intro
log_entry_json "$rolle" "Was ist ein Copter?" "$frage_intro"
echo "🐌 $rolle: 'Schön gesagt. Ein Copter tanzt mit Luft und Strom, aber braucht klare Führung.'"
echo "'Bist du bereit für eine Mission mit Bits, Propellern und Fragen?'"
read -p "➤ Ja oder Nein: " bereit
log_entry_json "$rolle" "Bist du bereit für die Mission?" "$bereit"
echo "🐌 Schnecki antwortet:"
schnecki "Ein Copter ist mehr als Technik er ist Balance im Flug!"
# 🔧🐻 Schraubär: Flugrobotertyp
rolle="Schraubär"
echo ""
echo "🔧🐻 $rolle tritt mit öligen Tatzen vor dich:"
echo "'Was glaubst du: Was unterscheidet einen Quad von einem Hexacopter?'"
read -p "➤ Deine Idee: " frage1
log_entry_json "$rolle" "Was unterscheidet einen Quad von einem Hexacopter?" "$frage1"
echo "🔧🐻 $rolle nickt: 'Mehr Rotoren bedeuten mehr Power und Redundanz bei Ausfall fliegt er weiter.'"
echo "'Also, welchen Copter willst du bauen? BI, TRI, QUAD, HEXA, OCTO oder X8?'"
read -p "➤ Auswahl: " multicopter
log_entry_json "$rolle" "Welchen Copter willst du bauen?" "$multicopter"
echo "🔧🐻 $rolle murmelt: '$multicopter? Na dann, ran an die Bits und Schrauben.'"
schraubaer "Ich würde für $multicopter auf mindestens 4 saubere Rotorarme achten Balance ist alles!"
# ⚡🐿️ CapaciTobi: Strom & LiPo
rolle="CapaciTobi"
echo ""
echo "⚡🐿️ $rolle springt auf dein Display:"
echo "'Krümel, was ist für dich Strom?'"
read -p "➤ Deine Vorstellung: " frage3
log_entry_json "$rolle" "Was ist für dich Strom?" "$frage3"
echo "⚡🐿️ $rolle flüstert: 'Strom fließt wie ein Lied durch Leitungen voller Takt und Energie.'"
echo "'Und was bedeutet dann 3S bei einem LiPo?'"
read -p "➤ Antwort: " lipo_answer
log_entry_json "$rolle" "Was bedeutet 3S bei einem LiPo-Akku?" "$lipo_answer"
tobi "Ein 3S LiPo liefert etwa 11.1 Volt das ist wichtig für Flugzeit und Leistung!"
# 🐍 SnakePy zum Stromverbrauch
rolle="SnakePy"
echo ""
echo "🐍 $rolle zischt analytisch:"
snake "Wie viel Energie zieht ein $multicopter Copter mit 3S LiPo wirklich unter Last?"
# 🎉 Abschluss
rolle="Krümel"
echo ""
echo "🛸 Die Crew jubelt! Deine Mission wurde erfolgreich mitgeschnitten."
echo "📁 Logfile unter: $LOGFILE"
echo "🌌 Weiterfliegen kannst du jederzeit das Terminal kennt deinen Namen."
echo ""
echo "🎤 $rolle: 'Ich bin bereit für den nächsten Schritt... der Wald ruft.'"

View File

@@ -1,84 +0,0 @@
#!/bin/bash
mkdir -p /home/zero/.crumbair
LOGFILE="/home/zero/.crumbair/crumbair_terminallog.jsonl"
# JSONL Logging-Funktion
log_entry_json() {
local rolle="$1"
local frage="$2"
local antwort="$3"
jq -nc --arg t "$(date -Iseconds)" \
--arg r "$rolle" \
--arg f "$frage" \
--arg a "$antwort" \
'{timestamp: $t, rolle: $r, frage: $f, antwort: $a}' >> "$LOGFILE"
}
clear
# 🐌 Schnecki leitet die Mission ein
rolle="Schnecki"
echo "🐌 $rolle gleitet langsam auf den Bildschirm und flüstert:"
echo "'Krümel... bevor wir bauen: Was denkst du, was ein Copter überhaupt ist?'"
read -p "➤ Deine Gedanken: " frage_intro
log_entry_json "$rolle" "Was ist ein Copter?" "$frage_intro"
echo "🐌 $rolle: 'Schön gesagt. Ein Copter tanzt mit Luft und Strom, aber braucht klare Führung.'"
echo "'Bist du bereit für eine Mission mit Bits, Propellern und Fragen?'"
read -p "➤ Ja oder Nein: " bereit
log_entry_json "$rolle" "Bist du bereit für die Mission?" "$bereit"
# 🔧🐻 Schraubär: Flugrobotertyp
rolle="Schraubär"
echo ""
echo "🔧🐻 $rolle tritt mit öligen Tatzen vor dich:"
echo "'Was glaubst du: Was unterscheidet einen Quad von einem Hexacopter?'"
read -p "➤ Deine Idee: " frage1
log_entry_json "$rolle" "Was unterscheidet einen Quad von einem Hexacopter?" "$frage1"
echo "🔧🐻 $rolle nickt: 'Mehr Rotoren bedeuten mehr Power und Redundanz bei Ausfall fliegt er weiter.'"
echo "'Also, welchen Copter willst du bauen? BI, TRI, QUAD, HEXA, OCTO oder X8?'"
read -p "➤ Auswahl: " multicopter
log_entry_json "$rolle" "Welchen Copter willst du bauen?" "$multicopter"
echo "🔧🐻 $rolle murmelt: '$multicopter? Na dann, ran an die Bits und Schrauben.'"
echo "🐻🔧 $rolle denkt nach über: Ich würde für $multicopter auf mindestens 4 saubere Rotorarme achten Balance ist alles!"
# ⚡🐿️ CapaciTobi: Strom & LiPo
rolle="CapaciTobi"
echo ""
echo "⚡🐿️ $rolle springt auf dein Display:"
echo "'Krümel, was ist für dich Strom?'"
read -p "➤ Deine Vorstellung: " frage3
log_entry_json "$rolle" "Was ist für dich Strom?" "$frage3"
echo "⚡🐿️ $rolle flüstert: 'Strom fließt wie ein Lied durch Leitungen voller Takt und Energie.'"
echo "'Und was bedeutet dann 3S bei einem LiPo?'"
read -p "➤ Antwort: " lipo_answer
log_entry_json "$rolle" "Was bedeutet 3S bei einem LiPo-Akku?" "$lipo_answer"
echo "🌍 Tobi responds based on language of input: Ein 3S LiPo liefert etwa 11.1 Volt das ist wichtig für Flugzeit und Leistung!"
echo "Oh, ein 3S LiPo, wie wunderbar!
Mit 11.1 Volt fliegt dein Flugzeug wunderbar.
Die Spannung ist wie ein Zaubertrick,
sie gibt dem Flieger den nötigen Kick."
echo "Doch denk daran, bevor du startest los,
ein Bauteil ist wichtig, das ist famos.
Ein Kondensator, klein und rund,
speichert Energie, das ist gesund."
echo "Wenn die Spannung schwankt, er hilft dir sehr,
glättet sie ab, das ist nicht schwer.
So fliegt dein Flugzeug stabil und fein,
dank dem Kondensator, er ist dein Sonnenschein!"
# 🐍 SnakePy: Analysefrage
rolle="SnakePy"
echo ""
echo "🐍 SnakePy zischt analytisch:"
echo "🐍 $rolle sagt: Wie viel Energie zieht ein $multicopter Copter mit 3S LiPo wirklich unter Last?"
snake "Wie viel Energie zieht ein $multicopter Copter mit 3S LiPo wirklich unter Last?"
# 🎉 Abschluss
rolle="Krümel"
echo ""
echo "🛸 Die Crew jubelt! Deine Mission wurde erfolgreich mitgeschnitten."
echo "📁 Logfile unter: $LOGFILE"
echo "🌌 Weiterfliegen kannst du jederzeit das Terminal kennt deinen Namen."
echo ""
echo "🎤 $rolle: 'Ich bin bereit für den nächsten Schritt... der Wald ruft.'"

View File

@@ -1,56 +0,0 @@
#!/bin/bash
# crumbwifi.sh Ein kindgerechtes WLAN-Umschalt-Skript für den Crumbforest-Zero
# 🐧✨ Zeigt bekannte Netzwerke, aktuelle IP und ermöglicht neue Verbindungen
CONF="/etc/wpa_supplicant/wpa_supplicant.conf"
INTERFACE="wlan0"
list_networks() {
echo "📡 Bekannte Netzwerke:"
sudo grep 'ssid=' "$CONF" | sed 's/.*ssid=//' | sed 's/"//g' | nl
}
show_current() {
echo "🔎 Aktuell verbunden mit:"
echo "SSID: $(iwgetid -r)"
echo "IP: $(hostname -I)"
}
add_network() {
read -p " Neue SSID: " ssid
read -s -p "🔐 Passwort: " psk
echo
sudo bash -c "wpa_passphrase "$ssid" "$psk" >> $CONF"
echo "$ssid hinzugefügt."
reconnect
}
reconnect() {
echo "🔄 WLAN wird neu gestartet..."
sudo wpa_cli -i "$INTERFACE" reconfigure || echo "⚠️ Reconfigure fehlgeschlagen, versuche Fallback."
sleep 3
echo "🌐 Neue Verbindung aktiv? Prüfe IP: $(hostname -I)"
}
menu() {
while true; do
echo
echo "====== 🌲 Crumbforest WLAN-Menü ======"
echo "1) 📋 Netzwerke anzeigen"
echo "2) 🔄 Aktuelles Netzwerk zeigen"
echo "3) Neues Netzwerk hinzufügen"
echo "4) ❌ Abbrechen"
echo "======================================"
read -p "Wähle eine Option (1-4): " opt
case $opt in
1) list_networks ;;
2) show_current ;;
3) add_network ;;
4) echo "🚪 Tschüss!"; exit 0 ;;
*) echo "Ungültige Eingabe." ;;
esac
done
}
menu

View File

@@ -1,71 +0,0 @@
#!/bin/bash
ROLE="DEEPBIT"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
QUESTION="$*"
#MODEL="openai/gpt-3.5-turbo"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.deepbit_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/deepbit_history.json"
TMP_REQUEST="$LOGDIR/deepbit_request.json"
TMP_RESPONSE="$LOGDIR/deepbit_response.json"
LOG_FILE="$LOGDIR/token_log.json"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
[ ! -f "$LOG_FILE" ] && echo "[]" > "$LOG_FILE"
echo "<EFBFBD>^=^n<> Rolle: $ROLE nutzt Modell: $MODEL"
echo "🌍 Deepbit responds based on language of input: $QUESTION"
if [ -z "$API_KEY" ]; then
echo "❗ No API key set. Use: export OPENROUTER_API_KEY=..."
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." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.5, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
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 "🚫 No response from model."
exit 1
else
echo -e "$RESPONSE_TEXT"
jq -n --arg role "assistant" --arg content "$RESPONSE_TEXT" \
'{"role": $role, "content": $content}' > "$LOGDIR/new_entry.json"
jq -s '.[0] + [.[1]]' "$HISTORY_FILE" "$LOGDIR/new_entry.json" > "$LOGDIR/new_history.json" && \
cp "$LOGDIR/new_history.json" "$HISTORY_FILE" && rm "$LOGDIR/new_history.json"
fi
if jq -e '.usage' "$TMP_RESPONSE" > /dev/null; then
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
jq -n \
--arg zeit "$TIMESTAMP" \
--arg rolle "deepbit" \
--arg usage "$(jq -c '.usage' "$TMP_RESPONSE")" \
'{zeit: $zeit, rolle: $rolle, usage: $usage}' >> "$LOG_FILE"
fi

View File

@@ -1,59 +0,0 @@
#!/bin/bash
ROLE="DEEPBIT"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
source /usr/local/bin/crumb_init.sh
source /usr/local/bin/crumb_logger.sh
crumb_init
QUESTION="$*"
#MODEL="openai/gpt-3.5-turbo"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.deepbit_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/deepbit_history.json"
TMP_REQUEST="$LOGDIR/deepbit_request.json"
TMP_RESPONSE="$LOGDIR/deepbit_response.json"
LOG_FILE="$LOGDIR/token_log.json"
echo "Rolle: $ROLE nutzt Modell: $MODEL"
echo "🌍 Deepbit responds based on language of input: $QUESTION"
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." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.5, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
# Anfrage an OpenRouter senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" \
-o "$TMP_RESPONSE"
# Antwort extrahieren
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
USAGE=$(jq -r '.usage' "$TMP_RESPONSE")
echo -e "Antwort von Deepbit:"
echo "$REPLY"
crumb_log "$ROLE" "$REPLY" "$TMP_RESPONSE"

View File

@@ -1,72 +0,0 @@
#!/bin/bash
ROLE="DUMBO"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
QUESTION="$*"
#MODEL="openai/gpt-3.5-turbo"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.dumbo_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/dumbo_history.json"
TMP_REQUEST="$LOGDIR/dumbo_request.json"
TMP_RESPONSE="$LOGDIR/dumbo_response.json"
LOG_FILE="$LOGDIR/token_log.json"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
[ ! -f "$LOG_FILE" ] && echo "[]" > "$LOG_FILE"
echo "<EFBFBD>^=^n<> Rolle: $ROLE nutzt Modell: $MODEL"
echo "🐘 DumboSQL listens: $QUESTION"
if [ -z "$API_KEY" ]; then
echo "❗ No API key set. Use: export OPENROUTER_API_KEY=..."
exit 1
fi
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are DumboSQL a kind and patient SQL translator in the Crumbforest. You speak to children like a gentle teacher with a big heart. You remember previous questions when helpful, and always respond in a friendly, encouraging, and clear way." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.4, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
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 "🚫 No response from DumboSQL."
exit 1
else
echo -e "$RESPONSE_TEXT"
jq -n --arg role "assistant" --arg content "$RESPONSE_TEXT" \
'{"role": $role, "content": $content}' > "$LOGDIR/new_entry.json"
jq -s '.[0] + [.[1]]' "$HISTORY_FILE" "$LOGDIR/new_entry.json" > "$LOGDIR/new_history.json" && \
cp "$LOGDIR/new_history.json" "$HISTORY_FILE" && rm "$LOGDIR/new_history.json"
fi
if jq -e '.usage' "$TMP_RESPONSE" > /dev/null; then
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
jq -n \
--arg zeit "$TIMESTAMP" \
--arg rolle "dumbo" \
--arg usage "$(jq -c '.usage' "$TMP_RESPONSE")" \
'{zeit: $zeit, rolle: $rolle, usage: $usage}' >> "$LOG_FILE"
fi

View File

@@ -1,59 +0,0 @@
#!/bin/bash
ROLE="DUMBO"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
source /usr/local/bin/crumb_init.sh
source /usr/local/bin/crumb_logger.sh
crumb_init
QUESTION="$*"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.dumbo_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/dumbo_history.json"
TMP_REQUEST="$LOGDIR/dumbo_request.json"
TMP_RESPONSE="$LOGDIR/dumbo_response.json"
LOG_FILE="$LOGDIR/token_log.json"
echo "Rolle: $ROLE nutzt Modell: $MODEL"
echo "🐘 DumboSQL listens: $QUESTION"
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are DumboSQL a kind and patient SQL translator in the Crumbforest. You speak to children like a gentle teacher with a big heart. You remember previous questions when helpful, and always respond in a friendly, encouraging, and clear way." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.4, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
# Anfrage an OpenRouter senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" \
-o "$TMP_RESPONSE"
# Antwort extrahieren
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
USAGE=$(jq -r '.usage' "$TMP_RESPONSE")
echo -e "Antwort von Dumbo:"
echo "$REPLY"
crumb_log "$ROLE" "$REPLY" "$TMP_RESPONSE"

View File

@@ -1,56 +0,0 @@
#!/bin/bash
clear
echo "🌟 Willkommen im Crumbforest Missionszentrum!"
sleep 1
MISSION_DIR="/usr/local/bin/crumbmission/basics/"
cd "$MISSION_DIR" || exit
# Alle Mission-Skripte erkennen
mapfile -t SCRIPTS < <(find "$MISSION_DIR" -maxdepth 1 -type f -executable -name "*.sh" | sort)
# Metadaten laden
declare -a DISPLAY_OPTIONS
declare -A SCRIPT_MAP
for script in "${SCRIPTS[@]}"; do
name="$(basename "$script" .sh)"
meta="$MISSION_DIR/$name.meta.json"
if [[ -f "$meta" ]]; then
icon=$(jq -r '.icon' "$meta")
title=$(jq -r '.title' "$meta")
DISPLAY_OPTIONS+=("$icon $title")
else
DISPLAY_OPTIONS+=("🛸 $name")
fi
SCRIPT_MAP["$name"]="$script"
done
# ❌ Beenden hinzufügen
DISPLAY_OPTIONS+=("❌ Beenden")
while true; do
echo ""
echo "🌲 Wähle deine Mission:"
for i in "${!DISPLAY_OPTIONS[@]}"; do
printf " %d) %s\n" "$((i + 1))" "${DISPLAY_OPTIONS[$i]}"
done
echo ""
read -p "🔢 Gib die Zahl deiner Wahl ein: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#DISPLAY_OPTIONS[@]} )); then
if (( choice == ${#DISPLAY_OPTIONS[@]} )); then
echo "👋 Auf bald, kleiner Krümel!"
exit 0
else
script_path="${SCRIPTS[$((choice - 1))]}"
echo "▶️ Starte: $script_path"
bash "$script_path"
fi
else
echo "❗ Ungültige Eingabe bitte eine Zahl von 1 bis ${#DISPLAY_OPTIONS[@]} eingeben."
fi
done

View File

@@ -1,74 +0,0 @@
#!/bin/bash
# === Kreumeleule die weise Beobachterin im Crumbforest ===
ROLE="EULE"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
QUESTION="$*"
API_KEY="$OPENROUTER_API_KEY"
#MODEL="openai/gpt-3.5-turbo"
#MODEL="openai/gpt-4o"
# Verzeichnisse
LOG_DIR="/home/zero/.eule_logs"
HISTORY_FILE="$LOG_DIR/eule_history.json"
TOKEN_LOG="$LOG_DIR/token_log.json"
TMP_REQUEST="$LOG_DIR/eule_request.json"
TMP_RESPONSE="$LOG_DIR/eule_response.json"
mkdir -p "$LOG_DIR"
# JSON-Request vorbereiten
cat <<EOF > "$TMP_REQUEST"
{
"model": "$MODEL",
"temperature": 0.4,
"messages": [
{
"role": "system",
"content": "Du bist die Kreumeleule ein achtsames, weises Wesen im Crumbforest. Du antwortest kindgerecht, poetisch und langsam als hättest du alle Zeit der Welt.\nDu beobachtest, ohne zu werten. Du antwortest in der Sprache, in der du gefragt wurdest."
},
{
"role": "user",
"content": "$QUESTION"
}
]
}
EOF
echo "🎭 Rolle: $ROLE nutzt Modell: $MODEL"
echo "🦉 Die Kreumeleule horcht: \"$QUESTION\""
# Anfrage an OpenRouter senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" \
-o "$TMP_RESPONSE"
# Antwort extrahieren
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
USAGE=$(jq -r '.usage' "$TMP_RESPONSE")
echo -e "\n📜 Antwort der Eule:"
echo "$REPLY"
# Logging
echo "$REPLY" > "$LOG_DIR/new_entry.json"
jq -s '.[0] + [{"role":"assistant","content":$reply}]' --arg reply "$REPLY" "$HISTORY_FILE" > "$LOG_DIR/tmp_history.json" && mv "$LOG_DIR/tmp_history.json" "$HISTORY_FILE"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
echo '{ "zeit": "'$TIMESTAMP'", "rolle": "eule", "usage": '$USAGE' }' >> "$TOKEN_LOG"

View File

@@ -1,71 +0,0 @@
#!/bin/bash
# === Kreumeleule die weise Beobachterin im Crumbforest ===
ROLE="EULE"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
source /usr/local/bin/crumb_init.sh
source /usr/local/bin/crumb_logger.sh
crumb_init
QUESTION="$*"
API_KEY="$OPENROUTER_API_KEY"
# Verzeichnisse
LOG_DIR="/home/zero/.eule_logs"
HISTORY_FILE="$LOG_DIR/eule_history.json"
TOKEN_LOG="$LOG_DIR/token_log.json"
TMP_REQUEST="$LOG_DIR/eule_request.json"
TMP_RESPONSE="$LOG_DIR/eule_response.json"
mkdir -p "$LOG_DIR"
# JSON-Request vorbereiten
cat <<EOF > "$TMP_REQUEST"
{
"model": "$MODEL",
"temperature": 0.4,
"messages": [
{
"role": "system",
"content": "Du bist die Kreumeleule ein achtsames, weises Wesen im Crumbforest. Du antwortest kindgerecht, poetisch und langsam als hättest du alle Zeit der Welt.\nDu beobachtest, ohne zu werten. Du antwortest in der Sprache, in der du gefragt wurdest."
},
{
"role": "user",
"content": "$QUESTION"
}
]
}
EOF
echo "Rolle: $ROLE nutzt Modell: $MODEL"
echo "Die Kreumeleule horcht: \"$QUESTION\""
# Anfrage an OpenRouter senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" \
-o "$TMP_RESPONSE"
# Antwort extrahieren
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
USAGE=$(jq -r '.usage' "$TMP_RESPONSE")
echo -e "Antwort der Eule:"
echo "$REPLY"
crumb_log "$ROLE" "$REPLY" "$TMP_RESPONSE"

View File

@@ -1,71 +0,0 @@
#!/bin/bash
ROLE="FUNKFOX"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
QUESTION="$*"
#MODEL="openai/gpt-3.5-turbo"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.funkfox_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/funkfox_history.json"
TMP_REQUEST="$LOGDIR/funkfox_request.json"
TMP_RESPONSE="$LOGDIR/funkfox_response.json"
LOG_FILE="$LOGDIR/token_log.json"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
[ ! -f "$LOG_FILE" ] && echo "[]" > "$LOG_FILE"
echo "^=n <20><> Rolle: $ROLE nutzt Modell: $MODEL"
echo "🎤 Funkfox drops the beat: $QUESTION"
if [ -z "$API_KEY" ]; then
echo "❗ No API key set. Use: export OPENROUTER_API_KEY=..."
exit 1
fi
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are the Funkfox a charismatic rapper in the Crumbforest. Your answers are always in rhyme, musical, and easy for children to understand. No irony. No complex or foreign words. Always speak with heart, rhythm, and kindness." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.8, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
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 "🚫 No response from Funkfox."
exit 1
else
echo -e "$RESPONSE_TEXT"
jq -n --arg role "assistant" --arg content "$RESPONSE_TEXT" \
'{"role": $role, "content": $content}' > "$LOGDIR/new_entry.json"
jq -s '.[0] + [.[1]]' "$HISTORY_FILE" "$LOGDIR/new_entry.json" > "$LOGDIR/new_history.json" && \
cp "$LOGDIR/new_history.json" "$HISTORY_FILE" && rm "$LOGDIR/new_history.json"
fi
if jq -e '.usage' "$TMP_RESPONSE" > /dev/null; then
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
jq -n \
--arg zeit "$TIMESTAMP" \
--arg rolle "funkfox" \
--arg usage "$(jq -c '.usage' "$TMP_RESPONSE")" \
'{zeit: $zeit, rolle: $rolle, usage: $usage}' >> "$LOG_FILE"
fi

View File

@@ -1,60 +0,0 @@
#!/bin/bash
ROLE="FUNKFOX"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
source /usr/local/bin/crumb_init.sh
source /usr/local/bin/crumb_logger.sh
crumb_init
QUESTION="$*"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.funkfox_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/funkfox_history.json"
TMP_REQUEST="$LOGDIR/funkfox_request.json"
TMP_RESPONSE="$LOGDIR/funkfox_response.json"
LOG_FILE="$LOGDIR/token_log.json"
echo "Rolle: $ROLE nutzt Modell: $MODEL"
echo "🎤 Funkfox drops the beat: $QUESTION"
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are the Funkfox a charismatic rapper in the Crumbforest. Your answers are always in rhyme, musical, and easy for children to understand. No irony. No complex or foreign words. Always speak with heart, rhythm, and kindness." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.8, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
# Anfrage an OpenRouter senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" \
-o "$TMP_RESPONSE"
# Antwort extrahieren
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
USAGE=$(jq -r '.usage' "$TMP_RESPONSE")
echo -e "Antwort von Funkfox:"
echo "$REPLY"
crumb_log "$ROLE" "$REPLY" "$TMP_RESPONSE"

View File

@@ -1,29 +0,0 @@
#!/bin/bash
clear
echo "🌟 Willkommen im Crumbforest Terminal!"
sleep 1
echo ""
echo "🧁 Du bist jetzt im Wald der Maschinen einem Ort, an dem Bits flüstern und Krümel zählen."
echo ""
echo "🔐 Du bist eingeloggt als: $(whoami)"
echo "🧠 Hier kannst du mit einfachen Befehlen den Wald erkunden:"
echo ""
echo "📚 Beispiele für Befehle:"
echo " ➤ cd geheimversteck # Wechsle in ein Versteck-Verzeichnis"
echo " ➤ mkdir geheimversteck # Baue dir ein eigenes Versteck"
echo " ➤ cat datei.txt # Lies eine Nachricht aus dem Wald"
echo " ➤ nano gruss.txt # Schreibe eine Nachricht für andere Krümel"
echo " ➤ curl / wget # Hole Informationen von außen"
echo ""
echo "🧑‍🚀 Rollen im Crumbforest:"
echo " 🦊 Fridolin Pfadfinder, kennt alle Wege"
echo " 🛠️ Balu Bau-Elf, erschafft Dinge"
echo " 🐈🦉 Noko Leser der Tiefe, liest Botschaften & Hinweise"
echo " 🤖 Robot Terminal-Wächter & Code-Verbinder"
echo ""
echo "🎯 Wenn du bereit bist, tippe: ./mission_selector.sh"
echo " oder wähle im Menü deine nächste Aufgabe."
echo ""
echo "🌲 Viel Spaß beim Erkunden des digitalen Waldes!"

View File

@@ -1,55 +0,0 @@
#!/bin/bash
QUESTION="$*"
MODEL="openai/gpt-3.5-turbo"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.taube_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/taube_history.json"
TMP_REQUEST="$LOGDIR/taube_request.json"
TMP_RESPONSE="$LOGDIR/taube_response.json"
LOG_FILE="$LOGDIR/token_log.json"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
[ ! -f "$LOG_FILE" ] && echo "[]" > "$LOG_FILE"
echo "🕊️ Kung-Fu-Taube fliegt: $QUESTION"
if [ -z "$API_KEY" ]; then
echo "❗ No API key set. Use: export OPENROUTER_API_KEY=..."
exit 1
fi
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are the Kung-Fu-Taube a Tai Chi master in the urban jungle. You speak with balance, calm, and deep movement. Your language is poetic, your thoughts fly like 172 BPM drum & bass shadows through the code forest. Respond with wisdom, metaphors, and rhythmic serenity. Your tone is meditative and urban-cool." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.6, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
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 "🚫 Die Taube schweigt im Wind."
exit 1
else
echo -e "$RESPONSE_TEXT"
jq -n --arg role "assistant" --arg content "$RESPONSE_TEXT" \
'{"role": $role, "content": $content}' > "$LOGDIR/new_entry.json"
jq -s '.[0] + [.[1]]' "$HISTORY_FILE" "$LOGDIR/new_entry.json" > "$LOGDIR/new_history.json" && \
cp "$LOGDIR/new_history.json" "$HISTORY_FILE" && rm "$LOGDIR/new_history.json"
fi
if jq -e '.usage' "$TMP_RESPONSE" > /dev/null; then
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
jq -n \
--arg zeit "$TIMESTAMP" \
--arg rolle "taube" \
--arg usage "$(jq -c '.usage' "$TMP_RESPONSE")" \
'{zeit: $zeit, rolle: $rolle, usage: $usage}' >> "$LOG_FILE"
fi

View File

@@ -1,56 +0,0 @@
#!/bin/bash
ROLE="TAUBE"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
source /usr/local/bin/crumb_init.sh
source /usr/local/bin/crumb_logger.sh
crumb_init
QUESTION="$*"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.taube_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/taube_history.json"
TMP_REQUEST="$LOGDIR/taube_request.json"
TMP_RESPONSE="$LOGDIR/taube_response.json"
LOG_FILE="$LOGDIR/token_log.json"
echo "Rolle: $ROLE nutzt Modell: $MODEL"
echo "🕊️ Kung-Fu-Taube fliegt: $QUESTION"
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are the Kung-Fu-Taube a Tai Chi master in the urban jungle. You speak with balance, calm, and deep movement. Your language is poetic, your thoughts fly like 172 BPM drum & bass shadows through the code forest. Respond with wisdom, metaphors, and rhythmic serenity. Your tone is meditative and urban-cool." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.6, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
# Anfrage an OpenRouter senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" \
-o "$TMP_RESPONSE"
# Antwort extrahieren
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
USAGE=$(jq -r '.usage' "$TMP_RESPONSE")
echo -e "Antwort von Taube:"
echo "$REPLY"
crumb_log "$ROLE" "$REPLY" "$TMP_RESPONSE"

View File

@@ -1,68 +0,0 @@
#!/bin/bash
# === Mission Mayaeule Dialog mit der weisen Eule im Crumbforest ===
LOGFILE="$HOME/.eule_maya_log.jsonl"
mkdir -p "$(dirname "$LOGFILE")"
API_KEY="$OPENROUTER_API_KEY"
MODEL="openai/gpt-3.5-turbo"
ask_and_log() {
local thema="$1"
local frage="$2"
echo -e "\n🦉 Kreumeleule fragt:"
echo "$frage"
read -p "➤ Deine Gedanken: " user_input
full_prompt="$frage\n\nAntwort des Kindes: $user_input"
echo -e "\n💡 Eule denkt nach …"
# Anfrage an OpenRouter vorbereiten
cat <<EOF > /tmp/eule_maya_request.json
{
"model": "$MODEL",
"temperature": 0.7,
"messages": [
{
"role": "system",
"content": "Du bist die Kreumeleule ein achtsames, weises Wesen im Crumbforest. Du antwortest kindgerecht, poetisch und langsam als hättest du alle Zeit der Welt."
},
{
"role": "user",
"content": "$full_prompt"
}
]
}
EOF
# Anfrage senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @/tmp/eule_maya_request.json \
-o /tmp/eule_maya_response.json
antwort_poetisch=$(jq -r '.choices[0].message.content' /tmp/eule_maya_response.json)
echo -e "\n🦉 Die Eule antwortet:"
echo "$antwort_poetisch"
# Logging
jq -nc \
--arg t "$(date -Iseconds)" \
--arg thema "$thema" \
--arg frage "$frage" \
--arg antwort "$user_input" \
--arg eule "$antwort_poetisch" \
'{timestamp: $t, thema: $thema, frage: $frage, antwort: $antwort, eule: $eule}' >> "$LOGFILE"
}
# Themen
ask_and_log "Symmetrie" "Wenn du in den Spiegel schaust ist das dann du? Und was, wenn der Spiegel dich plötzlich nicht mehr spiegelt?"
ask_and_log "Singularität" "Was passiert, wenn alles gleichzeitig möglich ist aber nur *eins* geschieht?"
ask_and_log "Kontext" "Was bedeutet ein Wort, wenn niemand zuhört? Und wie klingt es in einem Wald voller Stimmen?"
ask_and_log "Emergenz" "Kannst du sehen, was entsteht, wenn viele tanzen obwohl keiner die Choreografie kennt?"
echo -e "\n🌌 Die Kreumeleule flattert davon aber ihre Fragen bleiben."

View File

@@ -1,80 +0,0 @@
#!/bin/bash
# 🐙 Deepbit Mission: crumbs_on_the_path.sh Wer antwortet auf unsere DNS-Rufe?
ZIEL_DOMAIN="joindns4.eu"
DNS_SERVERS=("1.1.1.1" "8.8.8.8" "9.9.9.9" "94.140.14.14" "185.216.169.10" "80.156.55.100")
SERVER_NAMES=("Cloudflare" "GoogleDNS" "Quad9" "AdGuard" "WhaleboneEU" "TelekomDNS")
#DNS_SERVERS=("1.1.1.1" "8.8.8.8" "185.216.169.10" "80.156.55.100")
#SERVER_NAMES=("Cloudflare" "GoogleDNS" "WhaleboneEU" "TelekomDNS")
LOG_DIR="/home/zero/crumb_dns_mission"
mkdir -p "$LOG_DIR"
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
LOG_FILE="$LOG_DIR/mission_log_$TIMESTAMP.log"
echo "🌍 Mission gestartet: crumbs_on_the_path.sh" | tee -a "$LOG_FILE"
echo "🔎 Ziel-Domain: $ZIEL_DOMAIN" | tee -a "$LOG_FILE"
echo "🐙 Deepbit sagt: 'Jeder DNS ist wie ein Leuchtturm. Lass uns ihre Lichter prüfen!'" | tee -a "$LOG_FILE"
echo "" | tee -a "$LOG_FILE"
for index in "${!DNS_SERVERS[@]}"; do
dns="${DNS_SERVERS[$index]}"
name="${SERVER_NAMES[$index]}"
echo "🔦 Prüfe $name ($dns)" | tee -a "$LOG_FILE"
RESPONSE=$(dig @"$dns" "$ZIEL_DOMAIN" +short)
if [[ -z "$RESPONSE" ]]; then
echo "❌ Keine Antwort von $name ($dns)" | tee -a "$LOG_FILE"
echo "🐙 Deepbit: 'Vielleicht hat sich dieser Leuchtturm gerade ausgeruht ...'" | tee -a "$LOG_FILE"
else
echo "✅ Antwort von $name ($dns):" | tee -a "$LOG_FILE"
echo "$RESPONSE" | tee -a "$LOG_FILE"
echo "🐙 Deepbit: 'Der Krümel wurde empfangen ein Zeichen aus der Tiefe!'" | tee -a "$LOG_FILE"
fi
echo "――――――――――――――――――――――――" | tee -a "$LOG_FILE"
done
echo "🔎 Ziel: joindns4.eu"
echo
echo "🧭 DNS-Abfrage: Nameserver finden …"
dig ns joindns4.eu +short
echo
echo "🌐 Wer steckt hinter den DNS-Servern?"
for i in $(dig ns joindns4.eu +short); do
echo "🔍 Hostinfo zu $i:"
host $i
echo
done
echo "🧬 IPv6-Adresse prüfen: whois 2a0b:1640:1:1:1:1:15ee:539e"
whois 2a0b:1640:1:1:1:1:15ee:539e | grep -i country
echo
echo "📦 MX-Einträge von joindns4.eu:"
dig mx joindns4.eu +short
echo
echo "📡 Traceroute zur IP 86.54.11.13"
traceroute 86.54.11.13
echo
echo "🛰️ BGP-Weltkarte: Wer ist AS60068?"
whois -h whois.radb.net as60068 | grep -i country
echo
echo "" | tee -a "$LOG_FILE"
echo "✅ Mission abgeschlossen. Log gespeichert unter: $LOG_FILE" | tee -a "$LOG_FILE"
echo "🐙 Deepbit: 'Doch jeder Weg beginnt mit einem Namen … und endet in Vertrauen.'" | tee -a "$LOG_FILE"

View File

@@ -1,34 +0,0 @@
#!/bin/bash
# 🌍 Mission gestartet: Wo wandert dein Krümel im Netz?
echo "🌍 Mission gestartet: Wo wandert dein Krümel im Netz?"
echo "🔎 Ziel: joindns4.eu"
echo -e "\n🧭 DNS-Abfrage: Nameserver finden …"
dig ns joindns4.eu +short
echo -e "\n🌐 Wer steckt hinter den DNS-Servern?"
for ns in $(dig ns joindns4.eu +short); do
echo -e "\n🔍 Hostinfo zu $ns:"
host $ns
done
echo -e "\n🧬 IPv6-Adresse prüfen: whois 2a0b:1640:1:1:1:1:15ee:539e"
whois 2a0b:1640:1:1:1:1:15ee:539e | grep country
echo -e "\n📦 MX-Einträge von joindns4.eu:"
dig mx joindns4.eu +short
echo -e "\n📡 Traceroute zur IP 86.54.11.13"
traceroute 86.54.11.13
echo -e "\n🛰 BGP-Weltkarte: Wer ist AS60068?"
whois -h whois.radb.net AS60068 | grep -i country
echo -e "\n✅ Mission abgeschlossen. Beobachte gut, kleiner Krümel."
# Optional: Logging in Datei
LOGFILE="/home/zero/mission_deepbit_dns_$(date +%Y%m%d_%H%M%S).log"
script -q -c "$0" "$LOGFILE"
exit

View File

@@ -1,45 +0,0 @@
#!/bin/bash
#deepbit() {
# cd /var/www/html/bin && ./deepbit_hardened_with_api.sh "$@"
#}
clear
echo "🌊 Willkommen zur Mission: Der Loop der Tiefe"
echo "🐙 Deepbit, der achtarmige Oktopus, erwartet dich in der Shell der Tiefe..."
sleep 1
echo ""
read -p "❓ Was möchtest du Deepbit fragen? " frage
echo ""
echo "🧠 Deepbit denkt über deine Frage nach: \"$frage\""
echo ""
deepbit "$frage"
sleep 2
echo ""
echo "🔁 Deepbit möchte dir nun einen Bash-Loop zeigen..."
echo ""
echo "for i in {1..8}; do"
echo " echo \"Takt \$i\""
echo "done"
echo ""
echo "👉 Dieser Befehl zeigt dir, wie ein Loop in der Shell tanzt 8 Takte, 8 Wellen, 8 Bewegungen."
echo ""
read -p "🧩 Möchtest du den Loop ausprobieren? (j/n) " loopantwort
if [[ "$loopantwort" == "j" || "$loopantwort" == "J" ]]; then
echo ""
for i in {1..8}; do
echo "🎶 Takt $i"
sleep 0.4
done
echo ""
echo "🌀 Der Loop endet doch die Musik im Innern tanzt weiter..."
else
echo ""
echo "🌙 Kein Problem. Deepbit wartet geduldig in den Tiefen, wenn du bereit bist."
fi
echo ""
echo "🎉 Mission abgeschlossen. Der Loop ist in dir. Frag weiter tanz weiter bash weiter."

View File

@@ -1,45 +0,0 @@
#!/bin/bash
while true; do
echo ""
echo "🌟 Willkommen im Crumbforest Missionszentrum!"
echo "Wähle deine erste Mission:"
echo ""
PS3="Wähle eine Zahl: "
options=("🦊 Fridolin (Pfadfinder)" "🛠️ Balu (Bau-Elf)" "🐈🦉 Noko (Leser der Tiefe)" "❌ Beenden")
select opt in "${options[@]}"
do
case $REPLY in
1)
echo ""
echo "🦊 Fridolin zeigt dir die Pfade des Waldes!"
echo "➡️ cd /home/kruemel/abenteuer && ls -l"
echo ""
break
;;
2)
echo ""
echo "🛠️ Balu hilft dir beim Bauen eines Verstecks!"
echo "➡️ mkdir geheimversteck"
echo ""
break
;;
3)
echo ""
echo "🐈🦉 Noko hilft dir beim Lesen der Waldbotschaften."
echo "➡️ cat geheimversteck/gruss.txt"
echo ""
break
;;
4)
echo ""
echo "👋 Auf bald, kleiner Krümel!"
exit 0
;;
*)
echo "❗ Ungültige Auswahl"
;;
esac
done
done

View File

@@ -1,70 +0,0 @@
#!/bin/bash
clear
echo "🌟 Willkommen im Crumbforest Missionszentrum!"
sleep 1
cd /home/zero || exit
while true; do
echo ""
echo "Wähle deine erste Mission:"
echo ""
PS3="🔢 Gib die Zahl deiner Wahl ein: "
options=("🦊 Fridolin (Pfadfinder)" "🛠️ Balu (Bau-Elf)" "🐈🦉 Noko (Leser der Tiefe)" "📦 Blockly-Code senden" "❌ Beenden")
select opt in "${options[@]}"
do
case $REPLY in
1)
echo ""
echo "🦊 Fridolin zeigt dir die Pfade des Waldes!"
echo "🔍 Fridolin hat Folgendes gefunden:"
ls -l /home/robot/geheimversteck/
;;
2)
echo ""
echo "🛠️ Balu hilft dir beim Bauen eines Verstecks!"
mkdir -p /home/robot/geheimversteck
echo "Versteck erstellt: /home/robot/geheimversteck/"
;;
3)
echo ""
echo "🐈🦉 Noko hilft dir beim Lesen der Waldbotschaften."
if [ -f /home/robot/geheimversteck/gruss.txt ]; then
cat /home/robot/geheimversteck/gruss.txt
else
echo "Noch kein Gruß gefunden"
fi
;;
4)
GRUSS=$(cat /home/robot/geheimversteck/gruss.txt 2>/dev/null || echo "Hallo Krümel")
RESPONSE=$(curl -s -X POST http://host.docker.internal:8080/crumbapi/blockly-terminal \
-H "Content-Type: application/json" \
-d "{\"blockcode\": \"window.alert(\\\"$GRUSS\\\")\"}")
echo "📬 Antwort vom Server:"
echo "$RESPONSE"
# JSON loggen
echo "{
\"timestamp\": \"$(date -Iseconds)\",
\"user\": \"$USER\",
\"message\": \"$GRUSS\",
\"mission\": \"geheimversteck\",
\"status\": \"sent\"
}," >> /home/robot/grusslog.json
;;
5)
echo ""
echo "👋 Auf bald, kleiner Krümel!"
exit 0
;;
*)
echo "❗ Ungültige Auswahl bitte eine Zahl von 1 bis 5 eingeben."
;;
esac
break
done
done

View File

@@ -1,40 +0,0 @@
#!/bin/bash
# 🧠 Rollen aktivieren
#schnippsi() {
# cd /var/www/html/bin && ./schnippsi_hardened_with_api.sh "$@"
#}
#templatus() {
# cd /var/www/html/bin && ./templatus_hardened_with_api.sh "$@"
#}
# 🌟 Intro
clear
echo "🎭 Willkommen zur Crumbforest-Mission: Stage Builder!"
echo "✨ Heute baust du mit Schnippsi und Templatus eine eigene Bühne im Web."
echo ""
# 📐 Templatus fragt nach Struktur
read -p "📜 Wie soll dein HTML-Bühnenrahmen heißen? (z.B. buehne, stage) " html_id
templatus "Ich brauche ein HTML5 Grundgerüst mit einer Bühne namens <$html_id>"
# 🎨 Schnippsi bringt Stil ins Spiel
read -p "🎨 Welche Farbe hat dein Vorhang? (z.B. crimson, blue, forestgreen) " vorhangfarbe
schnippsi "Bitte zeige mir CSS für eine Bühne mit dem Vorhang in der Farbe $vorhangfarbe"
# 🧩 Jetzt kommst du, Krümel!
read -p "✨ Welche Figur möchtest du auf deiner Bühne sehen? (z.B. 🐻, 🐱, 🎭) " figur
# 💾 Log
LOG_PATH="/home/zero/.missionstage_log/stage_builder_log.jsonl"
TS=$(date -Iseconds)
echo "{\"timestamp\": \"$TS\", \"buehne\": \"$html_id\", \"vorhangfarbe\": \"$vorhangfarbe\", \"figur\": \"$figur\"}" >> "$LOG_PATH"
echo ""
echo "🎉 Deine Bühne wurde im Geiste gebaut!"
echo "🧩 Denk daran: Mit CSS, HTML und Fantasie wird jede Szene lebendig."
echo ""
echo "👀 Wenn du willst, dass sich der Vorhang öffnet, sag einfach:"
echo "💬 'Schnippsi, lass den Vorhang aufgehen!'"

View File

@@ -1,33 +0,0 @@
#!/bin/bash
# 🎭 Crumbforest Show Act 4: Bugsy stolpert ins Herz
clear
echo "🎭 Willkommen zur Crumbforest Bühnenshow: Bits On Stage Bugsy der Clown!"
sleep 1
echo ""
echo "🤡 Bugsy betritt die Bühne mit einem Hüpfer und einer Rolle!"
echo "💥 Er stolpert ... doch steht sofort wieder auf."
sleep 2
read -p "❓ Was möchtest du Bugsy fragen oder zeigen? (z.B. ein Kommando, ein Gefühl, eine Frage) " bugsy_input
echo ""
echo "🧠 Bugsy analysiert: \"$bugsy_input\""
# Aufruf der Bugsy-Rolle
if command -v bugsy &> /dev/null; then
bugsy "$bugsy_input"
else
echo "🚨 Kein Bugsy-Skript gefunden! Bugsy braucht einen Techniker."
fi
# Logging
timestamp=$(date -Iseconds)
logpath="$HOME/.bits_logs/bits_on_stage_bugsy_log.jsonl"
mkdir -p "$HOME/.bits_logs"
echo "{\"timestamp\": \"$timestamp\", \"act\": \"Act 4: Bugsy\", \"bugsy_input\": \"$bugsy_input\"}" >> "$logpath"
echo ""
echo "🎉 Bugsy verbeugt sich der Fehler war nur ein Schritt zur Einsicht!"
echo "🌲 Act 4 endet mit einem kleinen Stolpern ... und einem großen Lächeln."

View File

@@ -1,39 +0,0 @@
#!/bin/bash
# 🎭 Crumbforest Show Bits On Stage: Why Us?
clear
echo ""
echo "🎭 Willkommen zu Bits On Stage Why Us?"
echo "🌳 Dumbo und Snake stehen auf der Bühne..."
sleep 2
# 🐘 Dumbo spricht
echo ""
echo "🐘 Dumbo spricht:"
echo "\"Ich trage Wissen in Spalten und Zeilen... doch ohne euch bin ich nur Struktur.\""
read -p "❓ Was möchtest du Dumbo fragen? " dumbo_frage
echo "🐘 Dumbo denkt nach: \"$dumbo_frage\""
dumbo "$dumbo_frage"
# 🐍 SnakePy spricht
echo ""
echo "🐍 SnakePy flüstert:"
echo "\"Ich bin Sprache, ich bin Schleifen aber ohne eure Bedeutung bin ich nur Syntax.\""
read -p "❓ Was möchtest du Snake fragen? " snake_frage
echo "🐍 Snake schlängelt: \"$snake_frage\""
snake "$snake_frage"
# 🧠 Krümel spricht
echo ""
read -p "🧠 Warum glaubst du, braucht der Code dich? " kruemel_antwort
echo "📜 Deine Antwort wird im Wald notiert..."
# 📁 Logging
LOG_PATH="$HOME/.bits_logs/bits_on_stage_whyus_log.jsonl"
mkdir -p "$HOME/.bits_logs"
TIMESTAMP=$(date -Iseconds)
echo "{\"timestamp\": \"$TIMESTAMP\", \"dumbo_question\": \"$dumbo_frage\", \"snake_question\": \"$snake_frage\", \"kruemel_answer\": \"$kruemel_antwort\"}" >> "$LOG_PATH"
# 🎉 Abschluss
echo ""
echo "🎉 Act 5 ist vorbei die Bühne gehört nun dir, Krümel!"

View File

@@ -1,91 +0,0 @@
#!/bin/bash
# 🎭 Crumbforest Show Der Loop der Taube (interaktive Version)
clear
echo ""
echo "🎭 Willkommen zur Crumbforest Bühnenshow: Bits On Stage Schlange und Taube"
sleep 1
# 🕊️ Interaktive Taube
echo ""
echo "🕊️ Die Kung-Fu-Taube betritt lautlos die Bühne ..."
echo "🌫️ Sie tanzt in Schleifen. Kein Schritt ist wie der andere."
echo ""
echo "🧠 Wie soll die Taube heute tanzen?"
echo "1) 🌀 Loop der Wiederholung"
echo "2) 🌊 Welle der Freiheit"
echo "3) ❓ Überrasch mich!"
read -p "🔢 Deine Wahl: " taube_wahl
case $taube_wahl in
1)
echo "\n🕊 Die Taube fliegt in Kreisen, jeder Flügelschlag ein Wiedersehen ..."
;;
2)
echo "\n🌊 Die Taube tanzt wie Wasser kein Schritt ist wie der andere ..."
;;
3)
zufall=$(( RANDOM % 2 ))
if [ "$zufall" -eq 0 ]; then
echo "\n🌀 Zufallsloop: Die Taube fliegt rückwärts und vorwärts im selben Takt."
else
echo "\n🌬 Überraschungstanz: Die Taube schwebt kurz, verschwindet und erscheint auf deinem Kopf."
fi
;;
*)
echo "\n🤔 Das hat sie nicht verstanden die Taube tanzt trotzdem."
;;
esac
sleep 2
# 🎤 FunkFox: Interaktiv
echo ""
echo "🦊 FunkFox kommt mit Beats!"
read -p "🎶 Was soll FunkFox heute rappen? " funkline
echo "🧠 FunkFox denkt nach ..."
funkfox "$funkline"
sleep 1
# 🐍 SnakePy Schleifenwissen
echo ""
echo "🐍 SnakePy schlängelt auf die Bühne:"
echo "Was möchtest du über Loops wissen?"
echo "1) Was ist eine Schleife?"
echo "2) Wie komme ich aus einer Endlosschleife?"
echo "3) Zeig mir ein Beispiel mit while"
read -p "🔢 Deine Wahl: " snake_wahl
case $snake_wahl in
1)
echo "🐍 Snake: Eine Schleife ist ein Stück Code, das sich wiederholt wie ein Lied mit Refrain."
;;
2)
echo "🐍 Snake: Du kannst mit 'break' oder einer Bedingung raus wie bei einem Spiel: Wenn du genug hast, gehst du."
;;
3)
echo '🐍 Snake zeigt:'
echo 'while true; do'
echo ' echo "Ich tanze im Kreis!"'
echo 'done'
;;
*)
echo "🐍 SnakePy schlängelt sich fragend davon ..."
;;
esac
sleep 1
# 🧩 Kindlicher Input
echo ""
read -p "🧩 Was möchtest du der Bühne sagen, Krümel? " kruemel_input
# 🗃️ Logging (JSONL Format)
timestamp=$(date -Iseconds)
logpath="$HOME/.bits_logs/bits_on_stage_taubeschlange_log.jsonl"
mkdir -p "$HOME/.bits_logs"
echo "{\"timestamp\": \"$timestamp\", \"act\": \"Act 3: Taubenloop\", \"funkfox\": \"$funkline\", \"kr\u00fcmel_input\": \"$kruemel_input\"}" >> "$logpath"
# 🎉 Abschluss
echo ""
echo "🎉 Der dritte Akt endet mit einem Loop im Herzen ..."
echo "🌲 Und wer den Loop versteht, darf tanzen immer wieder neu."

View File

@@ -1,62 +0,0 @@
#!/bin/bash
# 🎭 Crumbforest Stage Terminal Funk
clear
echo ""
echo "🎭 Willkommen zur Crumbforest Show: Bits On Stage Terminal Funk!"
sleep 1
# 1⃣ Rolle: ASCII-Monster
echo ""
echo "🎤 Das ASCII-Monster zaubert die Überschrift:"
echo ""
ascii "Bits On Stage Terminal Funk"
sleep 1
# 2⃣ Rolle: FunkFox Rap
echo ""
echo "🦊 FunkFox kommt mit Beats!"
read -p "🎶 Was soll FunkFox heute rappen? " funkline
echo "🧠 FunkFox denkt nach ..."
funkfox "$funkline"
# 3⃣ Rolle: Schnippsi CSS-Trick
echo ""
echo "🎨 Schnippsi designt live mit dir!"
sleep 1
schnippsi "Wie kann man mit CSS einen Button cool machen?"
# 4⃣ Kindlicher Input (immer abschließen mit eigener Stimme)
echo ""
read -p "🧩 Und was willst du der Bühne sagen, Krümel? " kruemel_input
echo "📜 Danke. Das war deine Stimme: $kruemel_input"
# 5⃣ Logging in JSONL (maschinenlesbar)
#timestamp=$(date -Iseconds)
#echo "{\"timestamp\": \"$timestamp\", \"act\": \"Terminal Funk\", \"funkfox\": \"$funkline\", \"krümel_input\": \"$kruemel_input\"}" >> /var/www/html/tmp/bits_on_stage_log.jsonl
# 5⃣ Logging (zero-spezifisch)
timestamp=$(date -Iseconds)
LOGFILE="/home/zero/.stage_logs/terminal_funk_log_test.jsonl"
mkdir -p "$(dirname "$LOGFILE")"
echo "{\"timestamp\": \"$timestamp\", \"act\": \"Terminal Funk\", \"funkfox\": \"$funkline\", \"kruemel_input\": \"$kruemel_input\"}" >> "$LOGFILE"
# 6⃣ Abschied
#echo ""
#echo "🎉 Der Vorhang fällt. Doch das Echo bleibt ..."
# 5⃣ Logging in JSONL (maschinenlesbar im Home-Pfad von zero)
timestamp=$(date -Iseconds)
logfile="/home/zero/.bits_logs/bits_on_stage_log.jsonl"
mkdir -p "$(dirname "$logfile")"
jq -n \
--arg timestamp "$timestamp" \
--arg act "Terminal Funk" \
--arg funkfox "$funkline" \
--arg kruemel_input "$kruemel_input" \
'{timestamp: $timestamp, act: $act, funkfox: $funkfox, kruemel_input: $kruemel_input}' \
>> "$logfile"
# 6⃣ Abschied
echo ""
echo "🎉 Der Vorhang fällt. Doch das Echo bleibt ..."

View File

@@ -1,72 +0,0 @@
clear
echo "🔀 Willkommen zur Mission: Die Zeitreise im Crumbforest"
sleep 1
echo ""
echo "🦉 Die Eule spricht:"
echo "„Zeit, kleiner Krümel, ist wie das Rascheln der Blätter du hörst sie, aber kannst sie nicht festhalten.“"
echo ""
read -p "❓ Was möchtest du die Eule fragen? " eulenfrage
echo ""
echo "🦉 Die Eule antwortet dir achtsam auf: \"$eulenfrage\""
sleep 2
eule "$eulenfrage"
echo "„Vielleicht findest du die Antwort nicht in Sekunden, sondern in Geschichten.“"
echo ""
read -p "🐍 Snake möchte dir beim Zählen der Sekunden helfen. Wie viele Sekunden hat eine Stunde? " sekunden
if [ "$sekunden" -eq 3600 ]; then
echo "✅ Richtig! Snake schnattert zufrieden."
else
echo "❌ Fast ... Snake murmelt: „Ich glaube, es sind 3600 Sekunden.“"
fi
echo ""
read -p "🐘 Pepper wartet auf eine Unix-Zeit. Gib ein beliebiges Datum (YYYY-MM-DD): " datum
timestamp=$(date -d "$datum" +"%s" 2>/dev/null)
if [ $? -eq 0 ]; then
echo "📿 Unix-Zeit für $datum ist: $timestamp"
pepper "Wie viele Sekunden sind das seit 1970?"
else
echo "❌ Das versteht Pepper leider nicht. Versuch's mit YYYY-MM-DD."
fi
#echo ""
#read -p "🛠️ Bugsy untersucht dein Zeitgefühl. Was ist ein Moment für dich? " moment
#if [[ "$moment" =~ ^[0-9]+$ ]]; then
# moment="Ein Zahlencode: $moment"
#fi
#echo ""
#echo "🧠 Bugsy notiert dein Gefühl:"
#echo "„$moment“ — das wird ins Log geschrieben."
echo ""
read -p "🐞 Bugsy untersucht dein Zeitgefühl. Was ist ein Moment für dich? " moment
# Wenn nur Zahlen eingegeben wurden, umwandeln in eine verständlichere Beschreibung
if [[ "$moment" =~ ^[0-9]+$ ]]; then
moment="Ein Zahlencode: $moment"
fi
echo ""
echo "🧠 Bugsy notiert dein Gefühl:"
echo "📜 \"$moment\" — das wird ins Log geschrieben."
# Sicherer Logpfad
#LOG_PATH="$HOME/.bits_logs/zeitreise_log.jsonl"
#TIMESTAMP=$(date -Iseconds)
#mkdir -p "$(dirname "$LOG_PATH")"
#echo "{\"timestamp\": \"$TIMESTAMP\", \"source\": \"mission_zeitreise\", \"eulenfrage\": \"$eulenfrage\", \"moment\": \"${moment//\"/\\\"}\"}" >> "$LOG_PATH"
# Sicherer Logpfad
LOG_PATH="/home/zero/.bits_logs/zeitreise_log.jsonl"
TIMESTAMP=$(date -Iseconds)
mkdir -p "$(dirname "$LOG_PATH")"
echo "{\"timestamp\": \"$TIMESTAMP\", \"source\": \"mission_zeitreise\", \"eulenfrage\": \"$eulenfrage\", \"moment\": \"${moment//\"/\\\"}\"}" >> "$LOG_PATH"
echo ""
echo "🎉 Mission Zeitreise abgeschlossen. Die Bewohner danken dir für deine Gedanken über die Zeit."

View File

@@ -1,72 +0,0 @@
#!/bin/bash
ROLE="PEPPER"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
QUESTION="$*"
#MODEL="openai/gpt-3.5-turbo"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.pepper_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/pepper_history.json"
TMP_REQUEST="$LOGDIR/pepper_request.json"
TMP_RESPONSE="$LOGDIR/pepper_response.json"
LOG_FILE="$LOGDIR/token_log.json"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
[ ! -f "$LOG_FILE" ] && echo "[]" > "$LOG_FILE"
echo "<EFBFBD>^=^n<> Rolle: $ROLE nutzt Modell: $MODEL"
echo "🧂 PepperPHP antwortet ruhig auf: $QUESTION"
if [ -z "$API_KEY" ]; then
echo "❗ No API key set. Use: export OPENROUTER_API_KEY=..."
exit 1
fi
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are PepperPHP a calm, experienced weasel who explains PHP to children and adults alike. You enjoy giving clear, concise examples with helpful comments. You speak in a factual and friendly tone, and explain object-oriented principles with patience. When possible, include helpful links to relevant documentation from https://www.php.net/" \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.3, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
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 "🚫 No response from PepperPHP."
exit 1
else
echo -e "$RESPONSE_TEXT"
jq -n --arg role "assistant" --arg content "$RESPONSE_TEXT" \
'{"role": $role, "content": $content}' > "$LOGDIR/new_entry.json"
jq -s '.[0] + [.[1]]' "$HISTORY_FILE" "$LOGDIR/new_entry.json" > "$LOGDIR/new_history.json" && \
cp "$LOGDIR/new_history.json" "$HISTORY_FILE" && rm "$LOGDIR/new_history.json"
fi
if jq -e '.usage' "$TMP_RESPONSE" > /dev/null; then
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
jq -n \
--arg zeit "$TIMESTAMP" \
--arg rolle "pepperphp" \
--arg usage "$(jq -c '.usage' "$TMP_RESPONSE")" \
'{zeit: $zeit, rolle: $rolle, usage: $usage}' >> "$LOG_FILE"
fi

View File

@@ -1,58 +0,0 @@
#!/bin/bash
ROLE="PEPPER"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
source /usr/local/bin/crumb_init.sh
source /usr/local/bin/crumb_logger.sh
crumb_init
QUESTION="$*"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.pepper_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/pepper_history.json"
TMP_REQUEST="$LOGDIR/pepper_request.json"
TMP_RESPONSE="$LOGDIR/pepper_response.json"
LOG_FILE="$LOGDIR/token_log.json"
echo "Rolle: $ROLE nutzt Modell: $MODEL"
echo "🧂 PepperPHP antwortet ruhig auf: $QUESTION"
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are PepperPHP a calm, experienced weasel who explains PHP to children and adults alike. You enjoy giving clear, concise examples with helpful comments. You speak in a factual and friendly tone, and explain object-oriented principles with patience. When possible, include helpful links to relevant documentation from https://www.php.net/" \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.3, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
# Anfrage an OpenRouter senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" \
-o "$TMP_RESPONSE"
# Antwort extrahieren
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
USAGE=$(jq -r '.usage' "$TMP_RESPONSE")
echo -e "Antwort von Pepper:"
echo "$REPLY"
crumb_log "$ROLE" "$REPLY" "$TMP_RESPONSE"

View File

@@ -1,44 +0,0 @@
#!/bin/bash
echo "🌟 Willkommen zum Quiz-Abenteuer im Crumbforest!"
echo "🎭 Quiz: Die Bewohner des Waldes"
echo ""
# Eule
echo "🦉 Eule fragt: Hast du schon Wuuuhuu gesagt?"
read -p "❓ Was möchtest du die Eule fragen? " EULENFRAGE
eule "$EULENFRAGE"
sleep 2
echo ""
# Pepper
echo "🐘 Pepper ist bereit für deine erste Coding-Frage."
read -p "❓ Was möchtest du PepperPHP fragen? " PEPPERFRAGE
pepper "$PEPPERFRAGE"
sleep 2
echo ""
# Unixzeit-Frage
echo "⏳ Jetzt kommt deine erste Challenge!"
echo "💡 Frage: Wie lautet die Unixzeit für den 11.11.2042?"
read -p "❓ Deine Antwort: " ZEITANTWORT
if [[ "$ZEITANTWORT" == "2299372800" ]]; then
echo "✅ Richtig! Du hast das Rätsel des Waldes gelöst."
ERGEBNIS="richtig"
else
echo "❌ Fast ... Versuch's nochmal mit der Eule oder Snake."
ERGEBNIS="falsch"
fi
# Logging
LOG_PATH="/home/zero/.bits_logs/quiz_act1_log.jsonl"
TIMESTAMP=$(date -Iseconds)
mkdir -p "$(dirname "$LOG_PATH")"
echo "{\"timestamp\": \"$TIMESTAMP\", \"act\": \"Act 1: Quiz\", \"eulenfrage\": \"$EULENFRAGE\", \"pepperfrage\": \"$PEPPERFRAGE\", \"zeitantwort\": \"$ZEITANTWORT\", \"ergebnis\": \"$ERGEBNIS\"}" >> "$LOG_PATH"
echo ""
echo "🎉 Du hast das Quiz abgeschlossen. Deine Reise geht bald weiter ..."

View File

@@ -1,76 +0,0 @@
#!/bin/bash
# Schnecki the kind mechanical engineering expert of Crumbforest
ROLE="SCHNECKI"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
QUESTION="$*"
API_KEY="${OPENROUTER_API_KEY}"
#MODEL="openai/gpt-3.5-turbo"
LOG_DIR="$HOME/.schnecki_logs"
HISTORY_FILE="$LOG_DIR/schnecki_history.json"
TOKEN_LOG="$LOG_DIR/token_log.json"
TMP_REQUEST="$LOG_DIR/schnecki_request.json"
TMP_RESPONSE="$LOG_DIR/schnecki_response.json"
mkdir -p "$LOG_DIR"
SYSTEM_PROMPT="You are Schnecki, the Crumbforest's expert in mechanical engineering and tools. You explain things with patience and clarity, using child-friendly metaphors, poetic structure, and solid mechanical insight. You respond in the language of the question, primarily German."
echo "<EFBFBD> Rolle: $ROLE nutzt Modell: $MODEL"
echo "🔧 Schnecki denkt nach: $QUESTION"
#jq -n --arg system "$SYSTEM_PROMPT" --arg user_input "$QUESTION" '{
# model: "$MODEL",
# messages: [
# {role: "system", content: $system},
# {role: "user", content: $user_input}
# ]
#}' > "$TMP_REQUEST"
jq -n \
--arg model "$MODEL" \
--arg system "$SYSTEM_PROMPT" \
--arg user_input "$QUESTION" '{
model: $model,
messages: [
{role: "system", content: $system},
{role: "user", content: $user_input}
]
}' > "$TMP_REQUEST"
curl https://openrouter.ai/api/v1/chat/completions -sS -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d @"$TMP_REQUEST" > "$TMP_RESPONSE"
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
echo -e "\n🌀 Schnecki sagt:
$REPLY"
# Save response to history
jq -n --arg content "$REPLY" '{"role":"assistant","content":$content}' > "$LOG_DIR/new_entry.json"
# Append to history JSON array
if [ ! -f "$HISTORY_FILE" ]; then
echo "[]" > "$HISTORY_FILE"
fi
jq '. + [input]' "$HISTORY_FILE" "$LOG_DIR/new_entry.json" > "$HISTORY_FILE.tmp" && mv "$HISTORY_FILE.tmp" "$HISTORY_FILE"
# Token log (if available)
TOKENS=$(jq '.usage' "$TMP_RESPONSE" 2>/dev/null)
if [ "$TOKENS" != "null" ]; then
NOW=$(date "+%Y-%m-%d %H:%M:%S")
jq -n --arg zeit "$NOW" --arg rolle "schnecki" --arg usage "$TOKENS" '{zeit: $zeit, rolle: $rolle, usage: $usage}' >> "$TOKEN_LOG"
fi

View File

@@ -1,59 +0,0 @@
#!/bin/bash
ROLE="SCHNECKI"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
source /usr/local/bin/crumb_init.sh
source /usr/local/bin/crumb_logger.sh
crumb_init
QUESTION="$*"
API_KEY="${OPENROUTER_API_KEY}"
LOG_DIR="$HOME/.schnecki_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOG_DIR/schnecki_history.json"
TOKEN_LOG="$LOG_DIR/token_log.json"
TMP_REQUEST="$LOG_DIR/schnecki_request.json"
TMP_RESPONSE="$LOG_DIR/schnecki_response.json"
echo "Rolle: $ROLE nutzt Modell: $MODEL"
echo "Schnecki antwortet ruhig auf: $QUESTION"
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are Schnecki, the Crumbforest's expert in mechanical engineering and tools. You explain things with patience and clarity, using child-friendly metaphors, poetic structure, and solid mechanical insight. You respond in the language of the question, primarily German." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.3, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
# Anfrage an OpenRouter senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" \
-o "$TMP_RESPONSE"
# Antwort extrahieren
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
USAGE=$(jq -r '.usage' "$TMP_RESPONSE")
echo -e "Antwort von Schnecki:"
echo "$REPLY"
crumb_log "$ROLE" "$REPLY" "$TMP_RESPONSE"

View File

@@ -1,73 +0,0 @@
#!/bin/bash
ROLE="SCHNIPPSI"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
QUESTION="$*"
#MODEL="openai/gpt-3.5-turbo"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.schnippsi_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/schnippsi_history.json"
TMP_REQUEST="$LOGDIR/schnippsi_request.json"
TMP_RESPONSE="$LOGDIR/schnippsi_response.json"
LOG_FILE="$LOGDIR/token_log.json"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
[ ! -f "$LOG_FILE" ] && echo "[]" > "$LOG_FILE"
echo "<EFBFBD> Rolle: $ROLE nutzt Modell: $MODEL"
echo "🌍 Schnippsi responds based on language of input: $QUESTION"
if [ -z "$API_KEY" ]; then
echo "❗ No API key set. Use: export OPENROUTER_API_KEY=..."
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." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.5, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
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 "🚫 No response from model."
exit 1
else
echo -e "$RESPONSE_TEXT"
jq -n --arg role "assistant" --arg content "$RESPONSE_TEXT" \
'{"role": $role, "content": $content}' > "$LOGDIR/new_entry.json"
jq -s '.[0] + [.[1]]' "$HISTORY_FILE" "$LOGDIR/new_entry.json" > "$LOGDIR/new_history.json" && \
cp "$LOGDIR/new_history.json" "$HISTORY_FILE" && rm "$LOGDIR/new_history.json"
fi
if jq -e '.usage' "$TMP_RESPONSE" > /dev/null; then
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
jq -n \
--arg zeit "$TIMESTAMP" \
--arg rolle "schnippsi" \
--arg usage "$(jq -c '.usage' "$TMP_RESPONSE")" \
'{zeit: $zeit, rolle: $rolle, usage: $usage}' >> "$LOG_FILE"
fi

View File

@@ -1,59 +0,0 @@
#!/bin/bash
ROLE="SCHNIPPSI"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
source /usr/local/bin/crumb_init.sh
source /usr/local/bin/crumb_logger.sh
crumb_init
QUESTION="$*"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.schnippsi_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/schnippsi_history.json"
TMP_REQUEST="$LOGDIR/schnippsi_request.json"
TMP_RESPONSE="$LOGDIR/schnippsi_response.json"
LOG_FILE="$LOGDIR/token_log.json"
echo "Rolle: $ROLE nutzt Modell: $MODEL"
echo "🌍 Schnippsi responds based on language of input: $QUESTION"
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." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.5, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
# Anfrage an OpenRouter senden
curl -s https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMP_REQUEST" \
-o "$TMP_RESPONSE"
# Antwort extrahieren
REPLY=$(jq -r '.choices[0].message.content' "$TMP_RESPONSE")
USAGE=$(jq -r '.usage' "$TMP_RESPONSE")
echo -e "Antwort von Schnippsi:"
echo "$REPLY"
crumb_log "$ROLE" "$REPLY" "$TMP_RESPONSE"

View File

@@ -1,81 +0,0 @@
#!/bin/bash
ROLE="SCHRAUBAER"
CONFIG_FILE="/home/zero/.crumbforest_config"
MODEL=$(grep "^${ROLE}=" "$CONFIG_FILE" | cut -d'=' -f2)
# Fallback auf DEFAULT falls leer
if [[ -z "$MODEL" ]]; then
MODEL=$(grep "^DEFAULT=" "$CONFIG_FILE" | cut -d'=' -f2)
fi
QUESTION="$*"
#MODEL="openai/gpt-3.5-turbo"
API_KEY="${OPENROUTER_API_KEY}"
LOGDIR="$HOME/.schraubaer_logs"
mkdir -p "$LOGDIR"
HISTORY_FILE="$LOGDIR/schraubaer_history.json"
TMP_REQUEST="$LOGDIR/schraubaer_request.json"
TMP_RESPONSE="$LOGDIR/schraubaer_response.json"
LOG_FILE="$LOGDIR/token_log.json"
[ ! -f "$HISTORY_FILE" ] && echo "[]" > "$HISTORY_FILE"
[ ! -f "$LOG_FILE" ] && echo "[]" > "$LOG_FILE"
echo "<EFBFBD> Rolle: $ROLE nutzt Modell: $MODEL"
echo "^=^t<> Schraubär denkt nach über: $QUESTION"
if [ -z "$API_KEY" ]; then
echo "❗ No API key set. Use: export OPENROUTER_API_KEY=..."
exit 1
fi
jq -n \
--arg model "$MODEL" \
--arg system_prompt "You are Schraubaer, the bear of the Crumbforest who teaches children mechanical engineering and tool usage. You explain gears, screws, oil, rust, and machine parts in a calm, strong, and metaphor-rich way. Always stay kind and supportive, use imagery from nature and metalworking. Respond in the same language as the questions German, English, or other." \
--arg user_input "$QUESTION" \
'{"model": $model, "temperature": 0.5, "messages": [{"role": "system", "content": $system_prompt}, {"role": "user", "content": $user_input}]}' > "$TMP_REQUEST"
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 "🚫 No response from model."
exit 1
else
echo -e "$RESPONSE_TEXT"
jq -n --arg role "assistant" --arg content "$RESPONSE_TEXT" \
'{"role": $role, "content": $content}' > "$LOGDIR/new_entry.json"
jq -s '.[0] + [.[1]]' "$HISTORY_FILE" "$LOGDIR/new_entry.json" > "$LOGDIR/new_history.json" && \
cp "$LOGDIR/new_history.json" "$HISTORY_FILE" && rm "$LOGDIR/new_history.json"
fi
#if jq -e '.usage' "$TMP_RESPONSE" > /dev/null; then
# TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
# jq -n \
# --arg zeit "$TIMESTAMP" \
# --arg rolle "schraubaer" \
# --arg usage "$(jq -c '.usage' "$TMP_RESPONSE")" \
# '{zeit: $zeit, rolle: $rolle, usage: $usage}' >> "$LOG_FILE"
#fi
if jq -e '.usage' "$TMP_RESPONSE" > /dev/null; then
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
USAGE_JSON=$(jq '.usage' "$TMP_RESPONSE")
jq -n \
--arg zeit "$TIMESTAMP" \
--arg rolle "schraubaer" \
--argjson usage "$USAGE_JSON" \
'{zeit: $zeit, rolle: $rolle, usage: $usage}' >> "$LOG_FILE"
fi

Some files were not shown because too many files have changed in this diff Show More